Compare commits

...

683 Commits

Author SHA1 Message Date
Michael Telatynski 14b2ee2da4 Remove dependency on uuid (#5297)
Notify Downstream Projects / notify-downstream (element-web-notify, element-hq/element-web) (push) Has been skipped
Static Analysis / Typescript Syntax Check (push) Failing after 1m36s
Static Analysis / ESLint (push) Failing after 31s
Static Analysis / Node.js example (push) Failing after 42s
Static Analysis / Workflow Lint (push) Failing after 9m59s
Static Analysis / JSDoc Checker (push) Failing after 50s
Static Analysis / Analyse Dead Code (push) Failing after 36s
Static Analysis / Downstream tsc element-web (push) Has been skipped
Tests / Vitest [integ] (Node 22) (push) Failing after 4s
Tests / Vitest [unit] (Node 22) (push) Failing after 39s
Tests / Vitest [integ] (Node lts/*) (push) Failing after 39s
Tests / Vitest [unit] (Node lts/*) (push) Failing after 35s
Tests / Downstream test element-web (push) Has been skipped
Tests / Run Complement Crypto tests (push) Has been skipped
Static Analysis / Static Analysis (push) Successful in 1s
Tests / Tests (push) Failing after 1s
Tests / Downstream Complement Crypto tests (push) Successful in 1s
Tests / Downstream tests (push) Successful in 4s
2026-04-23 09:53:18 +00:00
renovate[bot] bb083222d9 Update dependency vite to v8 (#5295)
* Update dependency vite to v8

* Update lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-04-22 16:16:29 +00:00
Andy Balaam fa424c44b4 Support stable identifiers for MSC4268 (#5290)
* Support stable identifier m.room_key_bundle

* Support stable identifier m.shared_history

* Test that checks isRoomKeyBundleMessage works for stable and unstable identifiers

* Replace similar tests with use of it.each
2026-04-22 09:10:39 +00:00
renovate[bot] fef093747e Update dependency @typescript-eslint/eslint-plugin to v8.59.0 (#5292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 14:38:05 +00:00
renovate[bot] 4b33892d48 Update npm non-major dependencies (#5293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 13:58:22 +00:00
renovate[bot] d7d771fadb Update vite to v4.1.5 (#5291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 12:37:38 +00:00
Hubert Chathi 4ee3e591bf Handle secret pushing for key backups (#5189)
* push backup key to other verified devices when we reset backup

* handle receiving pushed backup keys

- make sure that backup gets enabled after we receive a pushed key that
  matches the current, valid backup

* apply requested changes from review
2026-04-20 21:48:51 +00:00
renovate[bot] 668183d722 Update vite to v4.1.4 (#5289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 11:25:37 +00:00
renovate[bot] 854dae0dc0 Update typescript (#5288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 10:01:43 +00:00
renovate[bot] 9d1aca2232 Update eslint-plugins (#5286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 09:11:28 +00:00
renovate[bot] 81569f3461 Update actions/setup-node digest to 48b55a0 (#5285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 08:50:10 +00:00
renovate[bot] 50783aba76 Update npm non-major dependencies (#5287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 08:17:20 +00:00
Michael Telatynski fd01b17236 Tidy knip config (#5284) 2026-04-17 13:00:21 +00:00
Will Hunt 25e92009b7 Throw more helpful errors from indexedb-local-backend (#5282)
* Throw more helpful errors from indexedb-local-backend

* Add a test

* Do not tempt fate
2026-04-17 08:33:56 +00:00
Michael Telatynski eb7acfb810 Add support for m.recent_emoji account data event (#5280) 2026-04-16 21:59:30 +00:00
Andy Balaam ca5655bced Rotate the room key when anyone leaves a room for any reason (#5279) 2026-04-16 12:09:42 +00:00
Michael Telatynski ef9b13e2a6 Fix coverage reports clobbering each other (#5267)
merge-multiple would silently drop files with clashing names - it ultimately isn't necessary given the `find` command will happily find them in nested subdirs
2026-04-16 11:48:51 +00:00
fkwp 159cca0363 Adapt LiveKit Identity hash calculation to latest MSC4195 update (#5268)
* adapt hash calculation to latest MSC4195 update.

Stop using '|' delimiters in hashes; use JSON arrays + canonical JSON instead

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* Update the test for RTC backend identities and add tests for calculating the identity hash.

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* linting

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* add copyright header

---------

Signed-off-by: fkwp <github-fkwp@w4ve.de>
2026-04-16 09:48:11 +00:00
renovate[bot] 3879111850 Update typescript (#5274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 11:37:50 +00:00
Andy Balaam f9a5aa87e3 Support the stable prefix for MSC4287 (key backup preference) (#5258)
* Support the stable prefix for MSC4287 (key backup preference)

* Remove incorrect doc coment on disableKeyStorage
2026-04-15 11:07:48 +00:00
renovate[bot] ed58df040c Update eslint-plugins (#5276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 09:51:25 +00:00
Michael Telatynski 0a3448d4c9 Fix references to matrix-react-sdk in CI (#5269) 2026-04-15 09:25:54 +00:00
renovate[bot] 25c1c1ea26 Update actions/github-script action to v9 (#5277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 09:11:09 +00:00
renovate[bot] a5e67af31f Update actions/upload-pages-artifact action to v5 (#5278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:37:38 +00:00
renovate[bot] b91e80814a Update zizmorcore/zizmor-action action to v0.5.3 (#5275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:37:07 +00:00
renovate[bot] d096a72605 Update npm non-major dependencies (#5273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:36:56 +00:00
renovate[bot] fb547e7b4b Update actions/upload-artifact digest to 043fb46 (#5272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:36:46 +00:00
Michael Telatynski 815294ca5a Allow oidc jwks_uri to be omitted (#5271)
* Allow oidc jwks_uri to be null

As signing keys are seemingly not (yet?) in the spec

* Add test coverage
2026-04-15 08:15:29 +00:00
Michael Telatynski 6d270b4685 Handle response_mode=fragment in completeAuthorizationCodeGrant (#5266)
* Handle response_mode=fragment in completeAuthorizationCodeGrant

* Add test

* Fix docs
2026-04-14 14:59:10 +00:00
Michael Telatynski 8bc3d96f6b Allow generating OIDC URIs with response_mode=fragment (#5265) 2026-04-13 22:53:05 +00:00
Richard van der Hoff b6ea6e105e Log clarifications for handleBackupSecretReceived (#5233)
Some changes to make this a bit easier to understand.
2026-04-10 12:33:30 +00:00
Valere Fedronic cd4e053fa5 Suppress and Reduce noisy logs for rtc (#5260) 2026-04-09 14:24:36 +00:00
Andy Balaam 727473af62 Expand the comment on CryptoApi.getUserDeviceInfo saying we request info from the server (#5256)
* Expand the comment on CryptoApi.getUserDeviceInfo saying we request info from the server

* Update comment to reflect waiting for in-progress requests, not making new ones

* Update the comment for userHasCrossSigningKeys too
2026-04-09 14:19:22 +00:00
Michael Telatynski f17f013f1e Satisfy pnpm audit (#5262) 2026-04-09 11:52:19 +00:00
renovate[bot] 9f4ab0b840 Update matrix-org/sonarcloud-workflow-action digest to 13968a2 (#5263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:28:46 +00:00
Michael Telatynski 6371e4b252 Update pnpm command for version bumping 2026-04-08 08:23:52 +01:00
Michael Telatynski b69929e01a Allow release-make to bump multiple package.json versions (#5261) 2026-04-07 14:44:15 +00:00
RiotRobot 9dc12baaa9 Merge branch 'master' into develop 2026-04-07 13:17:46 +00:00
RiotRobot 159738597d v41.3.0 2026-04-07 13:17:11 +00:00
Andy Balaam d02205652f Re-enable Complement Crypto tests (#5257)
Since https://github.com/matrix-org/complement-crypto/pull/235 these
should be more reliable.

This reverts commit 4d59291538.
2026-04-01 12:38:11 +00:00
Richard van der Hoff 5e03add29a Expose UserVerificationStatus.known flag (#5255)
Indicate whether we have a record of this user's identity.
2026-04-01 09:58:50 +00:00
renovate[bot] eeafd7fcaa Update npm non-major dependencies (#5251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 16:36:53 +00:00
renovate[bot] 78a3c5372d Update vite (#5253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:58:28 +00:00
renovate[bot] c0c3bc2a8c Update dependency p-retry to v8 (#5254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:28:47 +00:00
renovate[bot] c3ce49cabf Update pnpm to v10.33.0 (#5252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:19:11 +00:00
renovate[bot] 5408168dfd Update dependency eslint-plugin-jsdoc to v62.8.1 (#5250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:17:51 +00:00
renovate[bot] 61452ddc11 Update pnpm/action-setup action to v5 (#5239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:11:15 +00:00
Michael Telatynski d5160a5380 Add permissions for contents in workflow 2026-03-31 14:36:05 +01:00
Michael Telatynski 7ff4960a27 Update workflow to use build-and-test.yaml from EW
Moved in https://github.com/element-hq/element-web/pull/32929
2026-03-31 14:34:06 +01:00
RiotRobot 00f63db80f v41.3.0-rc.0 2026-03-31 12:33:30 +00:00
dependabot[bot] 9bcb83a20a Bump smol-toml from 1.6.0 to 1.6.1 (#5249)
Bumps [smol-toml](https://github.com/squirrelchat/smol-toml) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.6.0...v1.6.1)

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.6.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:59:06 +00:00
dependabot[bot] dd8d8e5410 Bump brace-expansion from 1.1.12 to 1.1.13 (#5248)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:41:40 +00:00
dependabot[bot] 32b8ff8116 Bump minimatch from 3.1.2 to 3.1.5 (#5247)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:40:24 +00:00
Skye Elliot 3ceadd512d Refactor history sharing tests using setupClients helper (#5235)
* tests: Refactor history sharing tests using `setupClients` helper

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Use separate destructors for test clients

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-30 11:13:37 +00:00
renovate[bot] 8182180550 Update dependency happy-dom to v20.8.9 [SECURITY] (#5244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-30 09:09:17 +00:00
renovate[bot] aed74c5a72 Update dependency happy-dom to v20.8.8 [SECURITY] (#5243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-27 20:09:41 +00:00
renovate[bot] 8c259c53a6 Update actions/deploy-pages action to v5 (#5238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 20:06:08 +00:00
Michael Telatynski 4d59291538 Disable Complement Crypto tests (#5242)
Disabled Complement Crypto tests due to flakiness.
2026-03-27 17:38:37 +00:00
Michael Telatynski 80009a1b31 Add support for non-root package.json for version calculation in Sonar workflow (#5240)
* Add support for non-root package.json for version calculation in Sonar workflow

* Fix version_cmd
2026-03-27 09:16:01 +00:00
renovate[bot] 93e6c95953 Update dependency typescript to v6 (#5234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 03:16:33 +00:00
renovate[bot] 27a5507cef Update dependency knip to v6 (#5237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 02:41:41 +00:00
renovate[bot] be06f6655e Update dependency typedoc to v0.28.18 (#5236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 22:56:30 +00:00
Skye Elliot 71152f33bf Rotate the current room key when we see a member leave (#5231)
* feat: Rotate room key when a member leaves the room

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* test: Assert room key rotated to prevent MSC4268 leaking keys

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* docs: Outline key rotation scenario above discard logic

* feat: Use `RoomStateEvents.Events` over membership event

* docs: Correct spelling in scenario explanation

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* docs: Pull scenario explanation up to `onRoomStateEvent`

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Assert room key is rotated under leave va gappy sync

* tests: Build sync response incrementally for gappy/ungappy sync

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-25 16:39:58 +00:00
RiotRobot bc8f67089c Merge branch 'master' into develop 2026-03-24 11:22:32 +00:00
RiotRobot acc9aa8939 v41.2.0 2026-03-24 11:21:57 +00:00
Richard van der Hoff e76f627fe3 Add some docs to the DeviceIsolationModes (#5232)
* Add some docs to the DeviceIsolationModes

Notes to help us/me remember how these relate to MSC4153.

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2026-03-23 15:29:35 +00:00
renovate[bot] 45b1e73842 Update npm non-major dependencies (#5229)
* Update npm non-major dependencies

* Update test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-20 09:58:04 +00:00
renovate[bot] f3eefd2f32 Update element-hq/element-meta digest to 7f2f93f (#5222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:27:19 +00:00
renovate[bot] f7c053216b Update dependency eslint-plugin-jsdoc to v62.8.0 (#5228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:18:31 +00:00
renovate[bot] 9c7739f14f Update actions/download-artifact digest to 3e5f45b (#5220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:10:38 +00:00
renovate[bot] 897afe153a Update pnpm/action-setup digest to fc06bc1 (#5225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 19:03:49 +00:00
renovate[bot] a929391dcd Update shogo82148/actions-upload-release-asset digest to 96bc1f0 (#5226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 18:06:53 +00:00
renovate[bot] 45c5ee9f65 Update actions/setup-node digest to 53b8394 (#5221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 16:11:41 +00:00
renovate[bot] e56aaa16c7 Update mheap/github-action-required-labels digest to 0ac283b (#5224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:47:21 +00:00
renovate[bot] da0d3d791e Update element-hq/element-web digest to 9730933 (#5223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:04:32 +00:00
renovate[bot] ed5eb670a1 Update zizmorcore/zizmor-action action to v0.5.2 (#5227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:27:14 +00:00
renovate[bot] b7fcb6e4c1 Update pnpm to v10.32.1 (#5230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:19:06 +00:00
RiotRobot bd775f6b61 v41.2.0-rc.0 2026-03-17 11:39:31 +00:00
Skye Elliot c2f9ad28fc Only share history if room history visibility is shared (#5216)
* feat: Only share history if room history visibility is shared

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* docs: Update documentation for `InviteOpts.shareEncryptedHistory`

* tests: Ensure shared history respects current history visibility

This commit additionally modifies `expectSendRoomEvent` to remove
the matcher on success, since fetchmock takes a while to do this
automatically.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-16 13:03:41 +00:00
Richard van der Hoff 7f33e3462e History sharing: resume key-bundle import on restart (#5214)
* Store rooms pending key bundles in the CryptoStore

Replace the in-memory storage of which rooms are waiting for a key bundle with
permanent storage in the crypto store.

* Clear pending-key-bundle flag on malformed bundles

If we cannot import the key bundle, there is no point trying again another
time: we may as well clear the flag either way.

* Factor out some helpers in history sharing integ test

* Do not accept key bundles for rooms we joined more than 24h ago

Per discussion in crypto-internal.

* Clear pending key bundle data when we leave a room

* Resume key-bundle import on restart

* Clear pending-key-bundle flag on rooms that we joined ages ago

* fixup! Clear pending-key-bundle flag on malformed bundles
2026-03-16 11:58:36 +00:00
Richard van der Hoff d99363d288 Move shareRoomHistoryWithUser to CryptoBackend (#5218)
There is no need for this method to be exposed to the application, and it's a
footgun waiting to trap the unwary user.

It's marked `@experimental` so we're allowed to move it without a major version
bump.
2026-03-12 21:21:31 +00:00
renovate[bot] 3642b99212 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v18 (#5217)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v18

* Adapt to breaking changes in rust-sdk wasm bindings

* more types fixes

* types fixes for tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2026-03-12 18:16:44 +00:00
Valere Fedronic 6ec0987286 re export sticky event types (#5213)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2026-03-12 17:27:11 +00:00
RiotRobot 219eb617dc Merge branch 'master' into develop 2026-03-10 13:46:12 +00:00
RiotRobot c6f9b25046 v41.1.0 2026-03-10 13:45:36 +00:00
Michael Telatynski 5d0e2efaf3 Add zizmor CI & make it happy (#5212)
* Add zizmor CI & make it happy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix additional zizmor warning

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-04 09:27:47 +00:00
renovate[bot] c7cd5570d3 Update eslint-plugins (#5207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 14:01:28 +00:00
renovate[bot] c2f6dd2ce0 Update crazy-max/ghaction-import-gpg action to v7 (#5210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:24:07 +00:00
renovate[bot] 393732aaae Update npm non-major dependencies (#5208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:23:47 +00:00
renovate[bot] d373fd8540 Update GitHub Artifact Actions (#5211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:22:31 +00:00
renovate[bot] 44a8a9a47a Update pnpm to v10.30.3 (#5209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:22:19 +00:00
RiotRobot 8a0b7ad68b v41.1.0-rc.0 2026-03-03 13:12:49 +00:00
Andy Balaam 09663302e1 Throw a specific error when the backup decryption key does not match the public backup (#5202) 2026-03-02 13:57:25 +00:00
dependabot[bot] 94f83b702c Bump rollup from 4.57.1 to 4.59.0 (#5203)
Bumps [rollup](https://github.com/rollup/rollup) from 4.57.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.57.1...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 13:48:20 +00:00
Michael Telatynski 9df27ee672 Fix downstream tsc (#5204)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-02 13:29:42 +00:00
Michael Telatynski 5739b59faa Update release workflows to deal with monorepos (#5201)
* Update release workflows to deal with monorepos

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Do the same for release-gitflow.yml

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-25 08:58:06 +00:00
Valere Fedronic e4425570c7 cleanup: Remove deprecated rtc room key transport (#5119)
* cleanup: Remove deprecated rtc room key transport

* fix: rtc statistics are managed by transport directly

* mark as readonly

* cleanup do not use deprecated `room`

* doc: Add missing param doc

* fixup: add back test wrongly removed
2026-02-24 13:32:08 +00:00
RiotRobot 145cb26054 Merge branch 'master' into develop 2026-02-24 13:59:12 +00:00
RiotRobot 26d5b1cde2 v41.0.0 2026-02-24 13:58:36 +00:00
renovate[bot] 0666d6b4e1 Update dependency typedoc to v0.28.17 (#5196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 11:23:34 +00:00
Michael Telatynski 9002064f10 Remove rimraf in favour of --delete-dir-on-start (#5200)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-24 11:01:56 +00:00
Will Hunt 5ea1554612 RTC Slots: Refactoring of membership event parsing and handling (#5134)
* Split out membership into seperate files.

* First pass of merging in new changes.

* More cleanup

* fix import

* Lots of test fixes

* remove skips

* unrelated change

* docstring

* comment

* lint lint lint

* copyright updates

* cleanup

* Ensure we await initial membership in all tests.

* fix race

* Use promises which are more reliable

* Even more promise stability

* cleanup

* Cleanup

* rename legacy.ts -> session.ts

* Update imports

* cleanup

* Rename files

* Rename + remove claimed_

* renaming

* Rename function

* All the cleanup

* tidy

* commit changes

* fix call membership

* fix claimed

* update slot_id

* fix device_id / claimed_device_id

* Update src/matrixrtc/utils.ts

Co-authored-by: R Midhun Suresh <hi@midhun.dev>

* use an aggregate error

* Export types

---------

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2026-02-24 10:51:23 +00:00
Will Hunt bd6547c081 Update getUrlPreview to use /_matrix/client/v1/media/preview_url (#5191)
* Update preview_url endpoint.

* support <v1.11

* Add tests
2026-02-23 19:49:05 +00:00
Michael Telatynski 8073f27d98 Specify this type for BaseLogger methods (#5198)
This allows https://typescript-eslint.io/rules/unbound-method/ to be happy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-19 23:05:15 +00:00
renovate[bot] 3bb22a9b28 Update npm non-major dependencies (#5197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 11:13:22 +00:00
renovate[bot] ba8bb3228d Update dependency eslint-plugin-jsdoc to v62.5.4 (#5195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 17:18:07 +00:00
renovate[bot] de23c9587b Update actions/checkout digest to de0fac2 (#5193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 17:17:30 +00:00
renovate[bot] 64eb482a49 Update actions/stale digest to b5d41d4 (#5194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 16:17:31 +00:00
RiotRobot aba7f8a0d4 v41.0.0-rc.0 2026-02-17 15:11:09 +00:00
RiotRobot 5495153c63 Merge remote-tracking branch 'origin/develop' into staging 2026-02-17 14:43:42 +00:00
Michael Telatynski 4f0696e2a4 Revert "Disable complement crypto tests temporarily (#5188)" (#5190)
This reverts commit 327d2fa7c8.
2026-02-17 11:13:05 +00:00
Bas Nijholt 0e659d294e fix(relations): prevent stale m.replace from overriding newer edits (#5192)
When multiple m.replace edits arrive concurrently, getLastReplacement()
may block on decryption. If an older edit's decryption completes after a
newer edit has already been applied, the older async result overwrites
the target event with stale content.

Add a monotonic update counter (replacementUpdateId) and centralise all
replacement updates through updateTargetEventReplacement(). The method
captures the counter before awaiting and discards the result if a newer
update has started in the meantime.

This race is especially pronounced in encrypted rooms with rapid
streaming-style edits, where variable decryption timing causes
out-of-order promise resolution.
2026-02-17 09:47:08 +00:00
Skye Elliot e74eb4928e Download room keys from backup prior to buliding historic room key bundles (#5171)
* chore: Update `@matrix-org/matrix-sdk-crypto-wasm` to v17.1.0

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* feat: Download keys from key backup.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Ensure backup is downloaded before building room key bundle.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* fix: Address review comments in history sharing tests.

* docs: Improve `getSyncResponse` and `assertInviteAndShareHistory` docs

* feat: Log `backupVersion`, `hasDecryptionKey` on download failure

* fix: Group backup data, add casts to `generate-test-data.py`.

* tests: Update `getSyncResponse` calls in history sharing integ tests

* fix: Add history visibility argument to state events test.

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-02-13 13:31:14 +00:00
David Baker 327d2fa7c8 Disable complement crypto tests temporarily (#5188)
* Disable complement crypto tests temporarily

As per comment

* and the downstream one too

* Stub downstream test instead

* prettier
2026-02-12 14:57:34 +00:00
Aditya Cherukuru 028357f15f Fix reactive display name disambiguation (#5135)
* Fix reactive display name disambiguation

When a room member changes their display name, recalculate the disambiguation flag for all other members who share (or previously shared) that display name. This ensures that the 'disambiguate' flag is updated reactively when display name conflicts appear or are resolved.

Fixes element-hq/element-web#468

Fixes element-hq/element-web#4795

Fixes element-hq/element-web#31551

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>

* Refactor: move disambiguation logic per review feedback

- Added updateDisambiguation() method to RoomMember for direct disambiguation recalculation

- Moved affected display name tracking to setStateEvents() instead of updateDisplayNameCache()

- Removed setMembershipEvent() hack, now calls updateDisambiguation() directly

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>

* Exclude processed members from disambiguation loop

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>

---------

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>
2026-02-12 14:23:04 +00:00
Michael Telatynski 872ec6755e Add support for Matrix Spec v1.13 (#5160) 2026-02-11 16:14:02 +00:00
RiotRobot 333d6a7bd6 v40.3.0-rc.0 2026-02-11 15:06:14 +00:00
Michael Telatynski 47532de452 Switch from yarn classic to pnpm (#5184) 2026-02-11 10:35:25 +00:00
RiotRobot 87e1049dae Merge branch 'master' into develop 2026-02-10 15:17:11 +00:00
RiotRobot 4c8e38009f v40.2.0 2026-02-10 15:16:24 +00:00
Timo 6e3efef0c5 Fix empty string to room compatibility trick to only apply to m.call (#5172)
* Fix empty string to room compatibility trick to only apply to m.call

* add logging

* fix linter

* Add tests

* limit logging.
2026-02-08 11:25:34 +00:00
Olivier 'reivilibre fb590627bb Add logging on MSC4108 DELETE request (#5140)
Just noticed these requests aren't logged,
which makes debugging difficult.
This is very drive-by, done in the web editor.

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2026-02-05 10:13:44 +00:00
Michael Telatynski 24cc17c270 Merge remote-tracking branch 'origin/develop' into develop 2026-02-04 11:47:05 +00:00
Michael Telatynski 68084e8fc3 Fix vitest slow reporter crashing CI
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-04 11:46:48 +00:00
Richard van der Hoff 0c3bb1f246 Add m.invite_permission_config account data type (#5183)
For MSC4380
2026-02-04 11:00:16 +00:00
renovate[bot] 7f42b67f68 Update matrix-org (#5181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 15:11:44 +00:00
renovate[bot] 9b871ac969 Update dependency eslint-plugin-jsdoc to v62.5.0 (#5179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:39:51 +00:00
renovate[bot] 49f7972a9e Update peter-evans/repository-dispatch digest to 28959ce (#5177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:16:42 +00:00
renovate[bot] c5ae4c8c0d Update dependency typedoc-plugin-mdn-links to v5.1.1 (#5180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:02:42 +00:00
renovate[bot] 2423300acd Update npm non-major dependencies (#5178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:01:03 +00:00
renovate[bot] 6cafa175b8 Update guibranco/github-status-action-v2 digest to 9bfa877 (#5176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 13:55:10 +00:00
RiotRobot 40165942e3 v40.2.0-rc.0 2026-02-03 13:14:44 +00:00
Hugh Nimmo-Smith f301251ff5 Clean up the ValidatedAuthMetadata types (#5175)
We don't expect oidc-client-ts to provide the `device_authorization_endpoint` in the `OidcMetadata` because it isn't part of the OIDC spec.

As such, I think it makes sense to standardise on defining the metadata fields in `validate.ts` and clarify where they come from.
2026-02-02 17:27:33 +00:00
Hugh Nimmo-Smith 21cd5e98c1 Use stable /auth_metadata endpoint where homeserver supports v1.15 (#5174) 2026-02-02 15:25:48 +00:00
Andy Balaam db070dca57 Support additional_creators in upgradeRoom (MSC4289) (#5173)
* Support additional_creators in upgradeRoom (MSC4289)

Signed-off-by: Andy Balaam <andy.balaam@matrix.org>

* Remove unneeded undefined in type definition

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-02 11:24:53 +00:00
Timo 8b6ff0abcb [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated (#5168)
* Make sure we do not call this.syncApi.injectRoomEvents before the
syncApi is instanciated

* sonarCube remove complexity (embedded.js constructor)
2026-01-29 16:22:41 +00:00
Timo f2157f28bb [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId (#5166)
* Minimal change to transition from "" to "ROOM" as the callId/slotId

* Also transition MembershipManager tests to `"ROOM"`

* fix merge
2026-01-29 15:37:34 +00:00
Timo 739b8e1f89 Remove sending of deprecated notify event (we now use (#5167)
`m.rtc.notification`
2026-01-27 15:52:41 +00:00
Timo bc57a3f829 [MatrixRTC] Do not sent the livekit_alias in sticky events (#5165)
* Do not sent the livekit_alias in sticky events

* tests
2026-01-27 15:23:14 +00:00
RiotRobot f136f6ddf7 Merge branch 'master' into develop 2026-01-27 12:39:59 +00:00
RiotRobot d428e7119a v40.1.0 2026-01-27 12:39:19 +00:00
ElementRobot 5532066178 Recalculate room name on loading members (#5158) (#5164)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-27 12:33:09 +00:00
R Midhun Suresh 82b51d0d46 Improve startup performance by using promise.all when processing rooms from sync (#5095)
* Use Promise.all when processing rooms in sync response

* Remove import
2026-01-27 08:56:26 +00:00
David Baker fb12a5a1d6 Recalculate room name on loading members (#5158)
* Recalculate room name on loading members

Because if it's a DM room, loading members might change the room name

* Swap other userA / userB constants

* Typo

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-26 12:25:41 +00:00
Hugh Nimmo-Smith 61ea5a7dfc Add OAuthGrantType enum for OAuth 2.0 API grant types (#5161)
* Add OAuthGrantType enum for OAuth 2.0 API grant types

* Resolve circular imports

* Fix complication error
2026-01-23 13:46:56 +00:00
Hugh Nimmo-Smith 4e032317fe Add support for stable OAuth2.0 aware feature from MSC3824 (#5159)
* Add support for stable OAuth2.0 aware feature from MSC3824

* Use stable name internally

* Mark DELEGATED_OIDC_COMPATIBILITY as

* Add tsdoc config for @alias JSDoc modifier
2026-01-23 09:50:12 +00:00
Robin dbb2ae5c07 Give RoomWidgetClient the ability to send and receive sticky events (#5142)
* Give RoomWidgetClient the ability to send and receive sticky events

* linter

* Fix existing tests

* Add tests for sticky event support in embedded clients

* Update sticky event widget capability identifiers

In matrix-widget-api 0.16.1 they are updated to use the new unstable prefix from MSC4407.

* Explicitly require matrix-widget-api ≥ 1.16.1

* remove TODO comment

* simplify type lint checks
This is needed for EW donwstream tests. Otherwise it will through:
Error: matrix-js-sdk/src/embedded.ts(417,21): error TS2345: Argument of
type 'string | number | boolean | string[]' is not assignable to
parameter of type 'number'.

---------

Co-authored-by: Timo K <toger5@hotmail.de>
2026-01-22 16:07:13 +00:00
Michael Telatynski c8032a214e Typescript fixes (#5157)
* Remove deprecated tsconfig baseUrl

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix imports in tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix duplicated map entry

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-21 17:28:10 +00:00
Timo 6e34ca6f2d [MatrixRTC] Fix delayId not resetting on leave (#5156)
* reset delay id to undefined after sucessfully sending it.

* add tests

* setAndEmitDelayId signature
dont allow implicit undefined
2026-01-21 13:53:55 +00:00
Andy Balaam 4a7a699623 Unit tests for OutgoingRequestsManager not repeating failed requests (#5154) 2026-01-21 12:03:09 +00:00
renovate[bot] 774776178e Update npm non-major dependencies (#5150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 15:08:00 +00:00
renovate[bot] ed4078528d Update dependency typedoc to v0.28.16 (#5149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 15:03:52 +00:00
renovate[bot] 7224961e9e Update actions/setup-node digest to 6044e13 (#5147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:56 +00:00
renovate[bot] 35ca07e8fb Update matrix-org/sonarcloud-workflow-action digest to ea0cd9d (#5148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:47 +00:00
renovate[bot] ccffb5df2d Update dependency eslint-plugin-jsdoc to v62 (#5153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:04 +00:00
renovate[bot] 0d162f66f3 Update dependency matrix-widget-api to v1.16.0 (#5152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:43:47 +00:00
RiotRobot bb7a689448 v40.1.0-rc.0 2026-01-20 13:57:24 +00:00
Andy Balaam c2b464a72c Avoid rapidly retrying failed requests (#5146)
After https://github.com/matrix-org/matrix-js-sdk/pull/5109 we retry
failed requests in a tight loop, instead of once every sync. When
requests are consistently failing, e.g. when /keys/uploads is failing
because of a duplicate OTK, this causes us to make many requests,
causing load on the server.

The fix is to reprocess the outgoing requests loop only if at least one
request succeeded in the last batch.

Fixes https://github.com/element-hq/element-web/issues/31790
2026-01-20 13:14:51 +00:00
Timo 4a75d2c92f [matrixRTC] MatrixRTCSessions, add missing event reemission. (#5144)
* add missing event reemission.

* review

* CI (SonarCube Code quality)
2026-01-19 10:49:37 +00:00
Michael Telatynski 899cdb0e1d Switch from Jest to Vitest (#5131)
* Skip unwritten tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy jest fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary sessionStorage mock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve async assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve error assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve object assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove assertion testing unclear mock

This test failed when ran individually, same as after the clearAllMocks call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Avoid awaiting non-thenables

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Pass nop function when stubbing out console, vitest won't accept it any other way

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary mock which causes tests to fail after updating fetch-mock & fix typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mistaken assertions not testing all values in array

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix hidden non-running tests in room.spec.ts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update fetch-mock-jest to @fetch-mock/jest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier 2.0

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch from Jest to Vitest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix CI

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update vite

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert irrelevant changes

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix coverage spec paths

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix slow test reporter

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix bad merge conflict resolution

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix babel config

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-15 11:15:37 +00:00
Michael Telatynski da7c6717fe Update fetch-mock-jest to @fetch-mock/jest (#5136)
* Skip unwritten tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy jest fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary sessionStorage mock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve async assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve error assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve object assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove assertion testing unclear mock

This test failed when ran individually, same as after the clearAllMocks call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Avoid awaiting non-thenables

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Pass nop function when stubbing out console, vitest won't accept it any other way

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary mock which causes tests to fail after updating fetch-mock & fix typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mistaken assertions not testing all values in array

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix hidden non-running tests in room.spec.ts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update fetch-mock-jest to @fetch-mock/jest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier 2.0

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-14 12:48:23 +00:00
Michael Telatynski b3fedf3a4e Prepare for jest->vitest (#5137)
* Skip unwritten tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy jest fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary sessionStorage mock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve async assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve error assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve object assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove assertion testing unclear mock

This test failed when ran individually, same as after the clearAllMocks call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Avoid awaiting non-thenables

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Pass nop function when stubbing out console, vitest won't accept it any other way

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary mock which causes tests to fail after updating fetch-mock & fix typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mistaken assertions not testing all values in array

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix hidden non-running tests in room.spec.ts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-13 15:19:50 +00:00
RiotRobot 3d0ebdf6f1 Merge branch 'master' into develop 2026-01-13 14:28:56 +00:00
RiotRobot 25555ec431 v40.0.0 2026-01-13 14:28:14 +00:00
Hugh Nimmo-Smith 33cd424f1f Add stable m.oauth UIA stage enum (#5138) 2026-01-13 08:37:23 +00:00
Robin 4d0d32307e Use normal base64 encoding for RTC backend identities (#5129)
* Use normal base64 encoding for RTC backend identities

MSC4195 has been updated to specify that normal (non-URL-safe) base64 is the correct encoding for LiveKit participant identities.

* Test RTC backend identity computation
2026-01-09 17:28:42 +00:00
Will Hunt b1a578f62e export parseCallNotificationContent and isMyMembership from types (#5132) 2026-01-09 16:30:17 +00:00
renovate[bot] 841b654c00 Update typescript (#4951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-08 18:56:40 +00:00
Richard van der Hoff ca0d4622b3 Deprecate unused EventShieldReason reason codes (#5127) 2026-01-08 18:14:33 +00:00
Richard van der Hoff bfd87a0896 Add MatrixEvent.getKeyForwardingUser (#5128)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v17

* Remove references to `ShieldStateCode.SentInClear`

This was never used, and is no longer exported, by rust-sdk-crypto-wasm, so we
need to remove references to it.

* Add `MatrixEvent.getKeyForwardingUser`

Expose information about keys forwarded via MSC4286, via a new method on
`MatrixEvent`.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 16:25:18 +00:00
renovate[bot] 6c59b0c22f Update dependency @matrix-org/matrix-sdk-crypto-wasm to v17 (#5126)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v17

* Remove references to `ShieldStateCode.SentInClear`

This was never used, and is no longer exported, by rust-sdk-crypto-wasm, so we
need to remove references to it.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2026-01-08 12:57:22 +00:00
Travis Ralston eff75c6525 Add types for (unstable) policy servers (#5116) 2026-01-08 00:19:57 +00:00
Valere Fedronic ee4a0b001e MatrixRTC: Cleaning up + address some sonarqube issues (#5125)
* cleanup: Remove deprecated API

* clean: breakdown method to reduce cognitive complexity

* cleanup: use readonly has never reassigned

* cleanup: Do not use an object literal as default

* quick format

* fixup: missed a param while refactoring

* cleanup: additional breakdown to reduce cognitive complexity

* review: better names
2026-01-07 13:20:54 +00:00
renovate[bot] 5fcd6fd744 Update npm non-major dependencies (#5123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 15:18:24 +00:00
renovate[bot] bf58f18b5f Update GitHub Artifact Actions (#5124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:30:16 +00:00
renovate[bot] 0c020b3ca4 Update dependency eslint-plugin-jest to v29.12.1 (#5122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:30:02 +00:00
renovate[bot] 2727ebb67f Update shogo82148/actions-upload-release-asset digest to 8f6863c (#5121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:29:53 +00:00
renovate[bot] 8fae4f3111 Update matrix-org/sonarcloud-workflow-action digest to 9f6f057 (#5120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:29:30 +00:00
RiotRobot 455b614008 v40.0.0-rc.0 2026-01-06 14:01:30 +00:00
Will Hunt 93f4f40202 Implement MSC4387: M_SAFETY error (#5107)
* Implement MatrixSafetyError

* Mention safety error on associated functions.

* fix import

* move error

* cleanup and add test

* wording

* fix test

* fixup error

* Fix exp
2026-01-06 13:04:52 +00:00
Richard van der Hoff aeade9ce58 Remove unused property MatrixEvent.untrusted (#5118)
* Remove unused property MatrixEvent.untrusted

This was never set to anything other than `false`. I think it is a hangover
from pre-rust-sdk.

* Remove call to redundant `isKeySourceUntrusted`

`isKeySourceUntrusted` always returns false so no point calling it

* Remove dangling assignments to MatrixEvent.untrusted
2026-01-06 11:23:13 +00:00
Timo 4b89fb23c5 MatrixRTC Pseudonymous livekit identities (#5110)
* deprecate membershipID -> memberId & memberId -> stateKey in membership
manager

The membership manager used the memberId label for the stateKey. But
only the StickymembershipManager really has a configurable memberId.

* participantId -> callMembershipIdentityParts

The participantId is a termonology from livekit. We do not want it in
here! We want the js-sdk to be mostly transport agnostic. We do the
transition from the identity parts to the acutal livekit identity in
Element call (`sha256(userId+deviceId+memberId)`)

* update tests

* Expose `kind` to decide if we use the hashed or non hashed livekit
participants.

* expose delayId from the matrixRTCSession for delayed event delegation.

* rename if to mapKey

* backandId computation as part of the js-sdk

* review valere

* valr + timo keysWithoutMatchingRTCMembership

* fix legacy encryption manager

* fix doc issue

* fix doc

* fix imports

* Encryption Manager needs own rtcBackendIdentity to use

The encryption manager needs to signal our own key fast, cannot wait for remote echo of rtc membership. So it needs to be able to compute the rtcBackendIdentity

* fix test

* Remove double `useHashedRtcBackendIdentity` assignment. rename
variables.

* little improvements This stops the usage from the matrix event outside
the CallMemerbship constructor.

* fix logger import

* Add back deprecated API for compat

* Make change to CallMembership constructor backward compatible

* more backward compatible

---------

Co-authored-by: Valere <bill.carson@valrsoft.com>
2026-01-05 18:40:11 +00:00
Robin 174439c2f0 Make MatrixRTC encryption key types narrower for TS 5.9 compatibility (#5117)
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-9.html#libdts-changes

TypeScript 5.9 changes some things about the ArrayBuffer type and makes a number of DOM types, including the subtle crypto APIs, require a narrower buffer type as their input. For example if you wanted to use crypto.subtle.importKey to convert a MatrixRTC encryption key buffer given by matrix-js-sdk to a CryptoKey, you would run into a type error with TS 5.9. Specifying the type parameter of Uint8Array everywhere around the MatrixRTC files fixes this breakage.
2026-01-05 17:42:21 +00:00
Richard van der Hoff 43f3e10f05 Improve documentation on rawDisplayName (#5114)
... since it's not actually the raw displayname, at all.
2025-12-18 14:57:14 +00:00
Andy Balaam 97fcdb2830 Make the enableEncryptedStateEvents property on MatrixClient public (#5113)
* Make the enableEncryptedStateEvents property on MatrixClient public

* fixup! Make the enableEncryptedStateEvents property on MatrixClient public

tsdoc for enableEncryptedStateEvents

* fixup! Make the enableEncryptedStateEvents property on MatrixClient public

Improve the description of enableEncryptedStateEvents
2025-12-18 13:57:20 +00:00
Andy Balaam 31e2d8eb20 Re-check outgoing requests after processing them (#5109)
... in case any new requests have been added during processing.
Fixes https://github.com/element-hq/element-web/issues/30988
2025-12-17 12:32:04 +00:00
Richard van der Hoff 633a5a8848 Mark forwardingCurve25519KeyChain as deprecated (#5111)
The Rust SDK always populates this as an empty array, so we may as well get rid
of it.
2025-12-16 14:05:26 +00:00
Richard van der Hoff a5086a09b9 Mark IEventDecryptionResult as deprecated (#5112)
This is supposed to be js-sdk-internal
2025-12-16 14:05:16 +00:00
RiotRobot c251be9ae5 Merge branch 'master' into develop 2025-12-16 13:51:14 +00:00
RiotRobot ec137cb5fb v39.4.0 2025-12-16 13:50:28 +00:00
David Baker ab4e24f115 Make token refresher init itself lazily (#5106)
* Make token refresher init itself lazily

It needs a network connection to do the init, so this would fail if
a client tried to do it at startup with no internet, causing the token
to just never be refreshed.

This just changes the API (compatibly) to do the init lazily.

The promise is kept is retain backwards compat, it can be removed
later.

* Make deviceId protected

* Fix tests
2025-12-12 18:23:43 +00:00
Will Hunt 2218ec4e31 Add _unstable_getRTCTransports to client. (#5104) 2025-12-11 17:35:54 +00:00
Michael Telatynski 319a8309c5 Update SonarCloud workflow action version 2025-12-11 16:35:10 +00:00
Timo 5af046f54f Use membershipID for session events (#5105)
* User membershipID for session events

* fix tests
2025-12-11 12:40:47 +00:00
Timo f97a9d9762 Remove three status cases that will never be set. (#5103) 2025-12-10 17:47:10 +00:00
renovate[bot] dc1a57a9f2 Update typescript-eslint monorepo to v8.48.1 (#5100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 17:52:39 +00:00
renovate[bot] 2d6111a04b Update actions/setup-node digest to 395ad32 (#5097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 16:14:08 +00:00
renovate[bot] 710fd7859d Update dependency eslint-plugin-jsdoc to v61.5.0 (#5101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:15:42 +00:00
renovate[bot] e340a4ceaf Update dependency @matrix-org/matrix-sdk-crypto-wasm to v16 (#5102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:15:31 +00:00
renovate[bot] 3fa44e076e Update actions/stale digest to 9971854 (#5098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:13:49 +00:00
renovate[bot] 3f9fb9c936 Update matrix-org/sonarcloud-workflow-action digest to 820f7c2 (#5099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:13:35 +00:00
renovate[bot] 8db347a75e Update actions/checkout digest to 8e8c483 (#5096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:13:17 +00:00
renovate[bot] 8db3343280 Update dependency prettier to v3.7.0 (#5090)
* Update dependency prettier to v3.7.0

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-09 15:12:27 +00:00
renovate[bot] 25c5a5b4ff Update dependency @casualbot/jest-sonar-reporter to v2.5.0 (#5003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:09:30 +00:00
RiotRobot a696e77652 v39.4.0-rc.0 2025-12-09 14:51:21 +00:00
Andy Balaam 582a76d87c Update encrypted state to say MSC4362 everywhere (#5079)
* Update encrypted state to say MSC4362 everywhere

* Fix test failure with encrypted state: handle empty string state key
2025-12-09 12:47:38 +00:00
Skye Elliot fdfddde55a Import room key bundles received after invite. (#5080)
* feat: Import room key bundles when received after invite.

* tests: Add spec test for room key bundle arriving after invite accepted.

* chore: Fix code quality issue (unnecessary async function).

* docs: Tidy up comments.

* refactor: Simplify key bundle importing after invite to one entrypoint.

- Remove `onReceiveToDeviceEvent` from `CryptoBackend`.
- Copy old room key bundle importing logic to
  `preprocessToDeviceEvents`.

* refactor: Move late bundle importing to main preprocess loop.

* fix: Use `Map` over `Record` to prevent prototype pollution.
2025-12-08 17:50:13 +00:00
Michael Telatynski 0ecfef2352 Update SonarCloud workflow action version 2025-12-04 13:22:19 +00:00
Michael Telatynski 4fdece6c1c Update SonarCloud action version in workflow 2025-12-04 13:14:18 +00:00
Michael Telatynski 6f0bce8708 Add label to skip Sonar coverage (#5094)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-04 12:22:38 +00:00
Michael Telatynski d3bdeb73f5 Avoid use of Optional type (#5093)
* Avoid use of Optional type

As we are likely to remove dependency on matrix-events-sdk

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tweak params

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-04 11:27:43 +00:00
RiotRobot 942fdf5bee Merge branch 'master' into develop 2025-12-02 14:45:10 +00:00
RiotRobot dd2635dbe6 v39.3.0 2025-12-02 14:44:30 +00:00
Will Hunt 3d1bcb73c1 Allow msc4354_sticky_key to be optional on sticky events. (#5073) 2025-11-27 11:54:49 +00:00
Michael Telatynski a960e686b3 Handle all response fields from /context API being optional (#5089)
* Handle all response fields from /context API being optional

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Simplify

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-27 11:12:39 +00:00
Michael Telatynski 946774c3fb Fix close-if-fork-develop job permissions (#5088)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-26 10:27:02 +00:00
renovate[bot] 15edbc8067 Update dependency @stylistic/eslint-plugin to v5.6.1 (#5083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 15:48:46 +00:00
renovate[bot] 1398ac24a2 Update typescript-eslint monorepo to v8.47.0 (#5086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 15:48:38 +00:00
renovate[bot] c76df4cd8f Update all non-major dependencies (#5082)
* Update all non-major dependencies

* Make knip happy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-25 15:43:23 +00:00
renovate[bot] a5e4dbf2d3 Update dependency matrix-widget-api to v1.15.0 (#5084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 14:58:14 +00:00
renovate[bot] 3768187395 Update eslint-plugins (#5085)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 14:58:04 +00:00
renovate[bot] 08d0ce25f1 Update actions/checkout action to v6 (#5087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 14:57:19 +00:00
RiotRobot 23241f18e2 v39.3.0-rc.0 2025-11-25 14:10:43 +00:00
Michael Telatynski 90da67aa95 Re-add truthy check on room name/avatar/alias events (#5081)
* Re-add truthy check on room name/avatar/alias events

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add regression test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-25 09:09:11 +00:00
renovate[bot] 0bf2702149 Update jest to v30 (major) (#4875)
* Update jest to v30

* Update snapshots & imports

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make jest happier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-24 09:07:13 +00:00
RiotRobot c7a75c8824 Merge branch 'master' into develop 2025-11-18 14:27:42 +00:00
RiotRobot 98b2b9745d v39.2.0 2025-11-18 14:26:56 +00:00
Michael Telatynski 65d5b3172c Fix invalid state events corrupting room objects (#5078)
* Fix invalid room name/canonical alias corrupting room objects

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-18 10:46:24 +00:00
renovate[bot] 2f72f9e889 Update dependency eslint-plugin-jsdoc to v61.1.12 (#5074)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 14:30:01 +00:00
renovate[bot] 18f500a1f8 Update all non-major dependencies (#5076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 14:01:33 +00:00
renovate[bot] b1df58796a Update typescript-eslint monorepo to v8.46.3 (#5075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 14:01:12 +00:00
RiotRobot 761b3771d6 v39.2.0-rc.0 2025-11-11 12:44:05 +00:00
Andrew Ferrazzutti df88edfda0 Delayed event management: split endpoints, no auth (#5066)
* Delayed event management: split endpoints, no auth

Add dedicated endpoints for each of the cancel/restart/send actions for
updating a delayed event, and make them unauthenticated.

Also keep support for the original endpoint where the update action is
in the request body, and make the split-endpoint versions fall back to
it if they are unsupported by the homeserver.

* Don't @link parameters in method docstrings

as TypeDoc doesn't support that

* Reduce code duplication

* Reduce code duplication again

* Add a little more test coverage

* Use split delayed event management for widgets

* Specify which eslint rule to ignore

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>

* Restore embedded non-split delay evt update method

Keep supporting it to not break widgets that currently use it.
Also add back the test for it.

* Deprecate the non-split delay evt update methods

* Comment to explain fallback to non-split endpoint

* Add backwards compatibility with authed endpoints

* Comment backwards compatibility helper method

* Await returned promises

because `return await promise` is at least as fast as `return promise`

---------

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
2025-11-11 05:54:33 +00:00
David Langley 1dee1ba581 Fix media switching during legacy calls (#5069)
* Specify exact for deviceId

* Update mediaHandler.spec.ts

* fallback to ideal if exact fails

* Reduce cognitive complexity for sonar

* Add tests
2025-11-10 09:36:32 +00:00
pkuzco b274c74a30 do not set cache in authenticated fetch (#5020)
* do not set cache in authenticated fetch

* issue 5019 - updated comment

* do not set cache in authenticated fetch

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>

* issue 5019 - updated comment

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>

---------

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>
Co-authored-by: Bahaa Naamneh <glimm.no@gmail.com>
2025-11-06 14:36:27 +00:00
RiotRobot b489bb15cf Merge branch 'master' into develop 2025-11-04 14:02:00 +00:00
RiotRobot dff4922a42 v39.1.2 2025-11-04 14:01:14 +00:00
Michael Telatynski dc6ad0b54c Fix npm publish script (#5068)
The `id` is also an output from this workflow so needs re-adding, not sure how the token stuck around, I thought I removed that
2025-11-04 13:33:55 +00:00
RiotRobot 9769c05dc5 Merge branch 'master' into develop 2025-11-04 13:23:41 +00:00
RiotRobot dd379d3d4c v39.1.1 2025-11-04 13:22:58 +00:00
Michael Telatynski 1b884a3e52 Fix npm release missing template braces (#5067) 2025-11-04 13:00:29 +00:00
RiotRobot ddb164490e Merge branch 'master' into develop 2025-11-04 12:54:27 +00:00
RiotRobot 796135c7ce v39.1.0 2025-11-04 12:53:45 +00:00
Michael Telatynski 4cc4c01dd8 Remove call to npm dist-tag (#5065)
As it does not work for NPM Trusted Publishing

https://github.com/npm/cli/issues/8547

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-30 09:34:14 +00:00
RiotRobot 533b40922c v39.1.0-rc.1 2025-10-30 08:45:29 +00:00
Michael Telatynski 0ae483ce27 Use NPM Trusted Publishers over token (#5064)
* Use NPM Trusted Publishers over token

due to security changes being enacted next month by npm

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update npm as ubuntu-latest only has 10.x and 11.5.1 or later is necessary

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-29 16:07:11 +00:00
renovate[bot] dbc1fa87ed Update dependency eslint-plugin-jsdoc to v61.1.8 (#5058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 23:03:02 +00:00
renovate[bot] 0a3675b971 Update typescript-eslint monorepo to v8.46.2 (#5059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 16:11:40 +00:00
renovate[bot] ab3f529d29 Update babel monorepo to v7.28.5 (#5057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:57:44 +00:00
renovate[bot] 607b712a07 Update dependency sdp-transform to v3 (#5062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:54:32 +00:00
renovate[bot] b6d9e49277 Update dependency @stylistic/eslint-plugin to v5.5.0 (#5061)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:54:17 +00:00
renovate[bot] 731d5943e2 Update GitHub Artifact Actions (#5063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:54:08 +00:00
renovate[bot] 01e7a43593 Update all non-major dependencies (#5060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:53:35 +00:00
RiotRobot b69a19ce7c v39.1.0-rc.0 2025-10-28 14:03:05 +00:00
Valere Fedronic 8703acb533 MatrixRTC: Disable room transport fallback for keys (#4929)
* matrixRTC: Disable room transport fallback for keys

* post rebase fix

* post merge fix

---------

Co-authored-by: Will Hunt <will@half-shot.uk>
2025-10-28 09:03:15 +00:00
Timo b59603d748 [MatrixRTC] Sticky Events support (MSC4354) (#5017)
* Implement Sticky Events MSC

* Renames

* lint

* some review work

* Update for support for 4-ples

* fix lint

* pull through method

* Fix the mistake

* More tests to appease SC

* Cleaner code

* Review cleanup

* Refactors based on review.

* lint

* Add sticky event support to the js-sdk

Signed-off-by: Timo K <toger5@hotmail.de>

* use sticky events for matrixRTC

Signed-off-by: Timo K <toger5@hotmail.de>

* make sticky events a non breaking change (default to state events. use joinConfig to use sticky events)

Signed-off-by: Timo K <toger5@hotmail.de>

* review
 - fix types (`msc4354_sticky:number` -> `msc4354_sticky?: { duration_ms: number };`)
  - add `MultiKeyMap`

Signed-off-by: Timo K <toger5@hotmail.de>

* Refactor all of this away to it's own accumulator and class.

* Add tests

* tidyup

* more test cleaning

* lint

* Updates and tests

* fix filter

* fix filter with lint

* Add timer tests

* Add tests for MatrixRTCSessionManager

* Listen for sticky events on MatrixRTCSessionManager

* fix logic on filtering out state events

* lint

* more lint

* tweaks

* Add logging in areas

* more debugging

* much more logging

* remove more logging

* Finish supporting new MSC

* a line

* reconnect the bits to RTC

* fixup more bits

* fixup testrs

* Ensure consistent order

* lint

* fix log line

* remove extra bit of code

* revert changes to room-sticky-events.ts

* fixup mocks again

* lint

* fix

* cleanup

* fix paths

* tweak test

* fixup

* Add more tests for coverage

* Small improvements

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* Document better

* fix sticky event type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix demo

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests

Signed-off-by: Timo K <toger5@hotmail.de>

* Update src/matrixrtc/CallMembership.ts

Co-authored-by: Robin <robin@robin.town>

* cleanup

* lint

* fix ci

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Half-Shot <will@half-shot.uk>
Co-authored-by: Robin <robin@robin.town>
2025-10-23 14:56:54 +00:00
Richard van der Hoff b0cbe22f64 Add CryptoApi.getSecretStorageStatus (#5054)
* Add `CryptoApi.getSecretStorageStatus`

`isSecretStorageReady` is a bit of a blunt instrument: it's hard to see from
logs *why* the secret storage isn't ready.

Add a new method which returns a bit more data.

* Update src/rust-crypto/rust-crypto.ts

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2025-10-23 11:04:28 +00:00
Timo 977d0322da Add parseCallNotificationContent (#5015)
* add parseCallNotificationContent

Signed-off-by: Timo K <toger5@hotmail.de>

* add tests

Signed-off-by: Timo K <toger5@hotmail.de>

* remove decline reason and better m.mentions check

Signed-off-by: Timo K <toger5@hotmail.de>

* cap ring duration to EX value (90s)

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-10-21 13:02:14 +00:00
RiotRobot dd7394c14c Merge branch 'master' into develop 2025-10-21 11:19:14 +00:00
RiotRobot f2d082064e v39.0.0 2025-10-21 11:18:29 +00:00
Andrew Ferrazzutti 2731e20893 MSC4140: support filters on delayed event lookup (#5038)
* MSC4140: support filters on delayed event lookup

Support looking up scheduled/finalised delayed events, and looking up a
single delayed event.

* Add test coverage for delayed event lookup filters

* Prettier

* Use it.each for test loop

* Support multiple delayIds

* Support single or multiple delayIds

As it may be more common to look up a single delayed event than to look
up many of them, support passing a single delayID in the lookup function
instead of needing to pass a single-element array.
2025-10-20 14:59:29 +00:00
renovate[bot] 502a513b5b Update eslint-plugins (#5050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 10:28:30 +00:00
renovate[bot] 3ac47e71cd Update eslint-plugins (#5049)
* Update eslint-plugins

* Bump matrix-org

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-15 09:49:42 +00:00
renovate[bot] 7c1e25e713 Update actions/setup-node action to v6 (#5047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 16:28:15 +00:00
renovate[bot] 2d90ad95f1 Update peter-evans/repository-dispatch action to v4 (#5048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:19:11 +00:00
renovate[bot] cd9794471f Update dependency @types/node to v18.19.130 (#5045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:17:41 +00:00
renovate[bot] b2d3ab8bc1 Update typescript-eslint monorepo to v8.46.0 (#5046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:17:11 +00:00
renovate[bot] d8b70ef83b Update actions/stale digest to 5f858e3 (#5043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:16:55 +00:00
renovate[bot] a67fb1fb8d Update all non-major dependencies (#5044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:13:48 +00:00
Will Hunt ddd6e77cde Handle redactions for sticky events (MSC4354) (#5037)
* Handle redactions.

* Add unit tests

* Refactor to support unstable insertion orders.

* lint

* tidy tidy

* Fix doc

* lint

* Assert userId

* Catch one occurance of sticky event errors not being caught
2025-10-14 13:49:31 +00:00
RiotRobot 2e9f5b6033 v39.0.0-rc.0 2025-10-14 13:34:57 +00:00
Timo fd949fe486 [MatrixRTC] Multi SFU support + m.rtc.member event type support (#5022)
* WIP

* temp

Signed-off-by: Timo K <toger5@hotmail.de>

* Fix imports

* Fix checkSessionsMembershipData thinking foci_preferred is required

* incorporate CallMembership changes
 - rename Focus -> Transport
 - add RtcMembershipData (next to `sessionMembershipData`)
 - make `new CallMembership` initializable with both
 - move oldest member calculation into CallMembership

Signed-off-by: Timo K <toger5@hotmail.de>

* use correct event type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix sonar cube conerns

Signed-off-by: Timo K <toger5@hotmail.de>

* callMembership tests

Signed-off-by: Timo K <toger5@hotmail.de>

* make test correct

Signed-off-by: Timo K <toger5@hotmail.de>

* make sonar cube happy (it does not know about the type constraints...)

Signed-off-by: Timo K <toger5@hotmail.de>

* remove created_ts from RtcMembership

Signed-off-by: Timo K <toger5@hotmail.de>

* fix imports

Signed-off-by: Timo K <toger5@hotmail.de>

* Update src/matrixrtc/IMembershipManager.ts

Co-authored-by: Robin <robin@robin.town>

* rename LivekitFocus.ts -> LivekitTransport.ts

Signed-off-by: Timo K <toger5@hotmail.de>

* add details to `getTransport`

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* use DEFAULT_EXPIRE_DURATION in tests

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test `does not provide focus if the selection method is unknown`

Signed-off-by: Timo K <toger5@hotmail.de>

* Update src/matrixrtc/CallMembership.ts

Co-authored-by: Robin <robin@robin.town>

* Move `m.call.intent` into the `application` section for rtc member events.

Signed-off-by: Timo K <toger5@hotmail.de>

* review on rtc object validation code.

Signed-off-by: Timo K <toger5@hotmail.de>

* user id check

Signed-off-by: Timo K <toger5@hotmail.de>

* review: Refactor RTC membership handling and improve error handling

Signed-off-by: Timo K <toger5@hotmail.de>

* docstring updates

Signed-off-by: Timo K <toger5@hotmail.de>

* add back deprecated `getFocusInUse` & `getActiveFocus`

Signed-off-by: Timo K <toger5@hotmail.de>

* ci

Signed-off-by: Timo K <toger5@hotmail.de>

* Update src/matrixrtc/CallMembership.ts

Co-authored-by: Robin <robin@robin.town>

* lint

Signed-off-by: Timo K <toger5@hotmail.de>

* make test less strict for ew tests

Signed-off-by: Timo K <toger5@hotmail.de>

* Typescript downstream test adjustments

Signed-off-by: Timo K <toger5@hotmail.de>

* err

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Robin <robin@robin.town>
2025-10-08 19:12:29 +00:00
Michael Telatynski 7b3aed8a47 Tidy knip config (#5036)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-08 14:19:25 +00:00
Will Hunt b84a73c7cc Implement Sticky Events MSC4354 (#5028)
* Implement Sticky Events MSC

* Renames

* lint

* some review work

* Update for support for 4-ples

* fix lint

* pull through method

* Fix the mistake

* More tests to appease SC

* Cleaner code

* Review cleanup

* Refactors based on review.

* lint

* Store sticky event expiry TS at insertion time.

* proper type
2025-10-07 17:24:10 +00:00
Michael Telatynski a03cf054a8 Only use the first 3 viaServers specified (#5034)
* Only use the first 3 viaServers specified

To avoid HTTP 414 URI Too Long error

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-07 13:14:13 +00:00
RiotRobot b3d217717a Merge branch 'master' into develop 2025-10-07 12:06:14 +00:00
RiotRobot 3e6fe5f914 v38.4.0 2025-10-07 12:05:30 +00:00
Andy Balaam a213d177f9 Fetch the user's device info before processing a verification request (#5030)
* Use checked way to get OlmMachine

* Factor out two variables in onKeyVerificationEvent

* Make sure verification test waits for the request to be processed

* Fetch the user's device info before processing a verification request

If we don't have the device info for a user when we receive their
verification request, we ignore it. This change gives us the best
possible chance of having the right device data before we try to process
the verification.

Fixes #30693
Fixes #27819
2025-10-07 08:13:31 +00:00
Will Hunt e885ecf08d Stabilise extended profiles (now part of Matrix v1.16) (#5013)
* Stabilise extended profiles (v1.16)

* lint

* Add capacity

* Update profile keys

* copyright

* lint
2025-10-06 15:10:15 +00:00
pkuzco d1d9aba745 feat(client): allow disabling VoIP support (#5021)
* feat(client): allow disabling VoIP support

* feat(client): allow disabling VoIP support

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>

* add a unit-test for disableVoip option

* fix lint issue

---------

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>
Co-authored-by: Bahaa Naamneh <glimm.no@gmail.com>
2025-10-01 16:52:36 +00:00
Michael Telatynski 52bcc2c955 Update community PR check to exclude dependabot (#5029) 2025-10-01 12:01:33 +00:00
renovate[bot] 1994806c72 Update typescript-eslint monorepo to v8.44.1 (#5025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:31:31 +00:00
renovate[bot] c4d1fd2c67 Update guibranco/github-status-action-v2 digest to 5530c59 (#5023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:18:48 +00:00
renovate[bot] 7ad8288525 Update dependency @types/node to v18.19.127 (#5024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:08:41 +00:00
renovate[bot] 31a42964e6 Update all non-major dependencies (#5026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:07:57 +00:00
renovate[bot] 2b1d37813c Update dependency @stylistic/eslint-plugin to v5.4.0 (#5027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:07:29 +00:00
RiotRobot e6fd0c58ff v38.4.0-rc.0 2025-09-30 12:41:53 +00:00
Will Hunt 41d70d0b5d Add call intent to RTC call notifications (#5010)
* Add media hint specifier

* Refactor to use m.call.intent and to apply to membership

* lint

* Add a mechanism to get the consensus of a call.

* Update tests

* Expose option to update the call intent.

* Better docs

* Add tests

* lint
2025-09-25 09:02:35 +00:00
Skye Elliot a08a2737e1 Implement experimental encrypted state events. (#4994)
* feat: Implement experimental encrypted state events.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* fix: Add cast from StateEvents[K] to IContent.

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-09-24 11:44:17 +00:00
Andy Balaam dbe441de33 Exclude cancelled requests from in-progress lists (#5016)
Fixes https://github.com/element-hq/element-web/issues/29882

When we ask for the in-progress verification requests, exclude requests
that have been cancelled. This means that we don't erroneously tell the
user that the new request they are about to create has been cancelled.
2025-09-23 13:51:19 +00:00
RiotRobot 9f3ca71495 Merge branch 'master' into develop 2025-09-23 12:13:04 +00:00
RiotRobot ef97df8ed0 v38.3.0 2025-09-23 12:12:22 +00:00
renovate[bot] 7f74fcc9f7 Update dependency debug to v4.4.3 (#5012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 11:30:33 +00:00
renovate[bot] e39644ad08 Update dependency @stylistic/eslint-plugin to v5.3.1 (#5002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 09:49:25 +00:00
renovate[bot] df0f0074b4 Update dependency uuid to v13 (#5009)
* Update dependency uuid to v13

* Make jest happy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 20:00:04 +00:00
renovate[bot] 29fbed5603 Update dependency p-retry to v7 (#5008)
* Update dependency p-retry to v7

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make jest happier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 18:45:28 +00:00
renovate[bot] 5ee6fc196b Update actions/stale action to v10 (#5007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 16:23:39 +00:00
renovate[bot] 961e32a3bb Update actions/setup-node action to v5 (#5006)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 15:30:02 +00:00
renovate[bot] 25f0418fce Update babel monorepo to v7.28.4 (#4998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 14:10:18 +00:00
RiotRobot 0ce751c462 v38.3.0-rc.0 2025-09-16 14:09:05 +00:00
renovate[bot] b7b3588cb8 Update shogo82148/actions-upload-release-asset digest to 59cbc56 (#4996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:58:13 +00:00
renovate[bot] 72846c713d Update actions/github-script action to v8 (#5005)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:56:33 +00:00
renovate[bot] 7c5229b4c8 Update typescript-eslint monorepo to v8.43.0 (#5004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:50:53 +00:00
renovate[bot] dd08388397 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v15.3.0 (#5001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:50:32 +00:00
renovate[bot] ec1ccebcca Update dependency typedoc to v0.28.13 (#5000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:46:32 +00:00
renovate[bot] e9b45cc504 Update dependency @types/node to v18.19.124 (#4999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:46:01 +00:00
renovate[bot] 5d4df65c09 Update all non-major dependencies (#4997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:45:36 +00:00
RiotRobot 2706873948 Merge branch 'master' into develop 2025-09-16 11:40:18 +00:00
RiotRobot 4a9006aea6 v38.2.0 2025-09-16 11:39:28 +00:00
Michael Telatynski 43c72d5bf5 Merge commit from fork
* Validate room upgrade relationships in MatrixClient::getJoinedRooms

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 12:36:14 +01:00
Andy Balaam e551b92a07 Update matrix-sdk-crypto-wasm to 15.2.0 (#4991)
* Update matrix-sdk-crypto-wasm to 15.2.0

Most relevant changes:

-   History sharing: improve efficiency of building key bundle
    ([matrix-rust-sdk#5513](https://github.com/matrix-org/matrix-rust-sdk/issues/5513))

* Work around matrix-rust-sdk#5643

Modify the message content coming from Rust API to include the missing
property `msgtype: m.key.verification.request`
2025-09-11 14:53:13 +00:00
Richard van der Hoff 32f51e852b History sharing: do /keys/query before checking for key bundle (#4992)
* History sharing: do `/keys/query` before checking for key bundle

The next release of matrix-sdk-crypto-wasm will check that the device that sent
us the key bundle data was correctly cross-signed by its owner, which means we
need to have the owner's cross-signing keys before we check if we have the
bundle.

This replicates a change made in the Rust SDK, at https://github.com/matrix-org/matrix-rust-sdk/pull/5510/files#diff-9f89fa75c4eb3743ae674be1bb90f75169bd815a259917799c71b8a546449d51R133-R140.

* fix unit test

* Comment
2025-09-11 10:38:14 +00:00
RiotRobot 82aa04d894 Merge branch 'master' into develop 2025-09-09 16:30:39 +00:00
RiotRobot ff89c9ec42 v38.1.0 2025-09-09 16:29:55 +00:00
Svajūnas Budrys ccd825fb39 Remove knock state on join (#4977)
Signed-off-by: Svajunas Budrys <svajunas.budrys.sb@gmail.com>
2025-09-04 09:31:26 +00:00
renovate[bot] b313eb5912 Update dependency @types/sdp-transform to v2.15.0 (#4987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 09:06:51 +00:00
renovate[bot] 2b12675675 Update typescript-eslint monorepo to v8.41.0 (#4989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:14:06 +00:00
renovate[bot] 246788b874 Update shogo82148/actions-upload-release-asset digest to e6cd457 (#4985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:13:57 +00:00
renovate[bot] b0b80401aa Update actions/upload-pages-artifact action to v4 (#4990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:39:36 +00:00
renovate[bot] 6a0164f37f Update dependency knip to v5.63.0 (#4988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:30:21 +00:00
renovate[bot] f963d61bcb Update typedoc (#4986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:29:33 +00:00
RiotRobot b32619ad24 v38.1.0-rc.0 2025-09-02 12:53:40 +00:00
Tulir Asokan 3d3c3ba55f Fix m.topic format (#4984)
* Fix m.topic format

Fixes #4902

Signed-off-by: Tulir Asokan <tulir@maunium.net>

* Update tests

Signed-off-by: Tulir Asokan <tulir@maunium.net>

* Fix formatting

* Re-add temporary support for invalid form

Signed-off-by: Tulir Asokan <tulir@maunium.net>

---------

Signed-off-by: Tulir Asokan <tulir@maunium.net>
2025-09-02 12:14:08 +00:00
Timo d62c658a72 Remove custom org.matrix.msc4075.rtc.notification.parent relation type (#4979)
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test

Signed-off-by: Timo K <toger5@hotmail.de>

* Fix test post rebase

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Valere <bill.carson@valrsoft.com>
2025-09-02 08:36:26 +00:00
David Baker bdc4a69023 Fix stable-suffixed MSC4133 support (#4983)
* Fix stable-suffixed MSC4133 support

It looked for the ".stable" suffixed feature to work out what URL to use but not to see whether the server supported it.

This will only be relevant until the next spec release but may as well fix it.

See also https://github.com/element-hq/element-web/pull/30649

* Fix awaiting
2025-09-01 15:34:01 +01:00
Bas Nijholt ab892420b5 Fix thread edit aggregation race condition (#4980)
* test(thread): add regression tests for edit-race; ensure reaction aggregation idempotence

- Edit race: add failing test when `Replace` aggregated pre-init
- Reaction: ensure pre-init aggregation and dedup on replay
- Strengthen assertions for ordering and content

* fix(thread): apply edits after init; keep reactions pre-init; remove redundant aggregation
- Defer Replace aggregation until thread initialised and event is in timeline
- Aggregate Annotation pre-init to preserve reaction summaries
- Rely on EventTimelineSet to aggregate post-insert
- Fixes: element-hq/element-web#30617

* style: run prettier; docs: clarify reaction pre-init comment about counts
2025-08-28 09:03:33 +00:00
RiotRobot ed607c48b0 Merge branch 'master' into develop 2025-08-27 13:32:25 +00:00
RiotRobot a8d75b81e5 v38.0.0 2025-08-27 13:31:35 +00:00
Timo 2f1d654f14 MatrixRTC: Add RTC decline event (#4978)
* Add decline event

Signed-off-by: Timo K <toger5@hotmail.de>

* add `client.sendRtcDecline`.

Signed-off-by: Timo K <toger5@hotmail.de>

* remove `decline.reason` field.

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-26 15:21:40 +00:00
Timo c4c7f94514 Make a MatrixRTCSession emit once the RTCNotification is sent (#4976)
* MatrixRTCSession emits once the rtc notification is sent.

Signed-off-by: Timo K <toger5@hotmail.de>

* update correct type description

Signed-off-by: Timo K <toger5@hotmail.de>

* Add test

Signed-off-by: Timo K <toger5@hotmail.de>

* fix imports

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-26 15:16:02 +00:00
David Baker 1fac06e223 Use hydra semantics for unknown room versions (#4957)
This inverts the check for whether to use hydra semantics to only NOT use
it for known, old room versions and use hydra for everything else, so
rooms with versions we don't know about will use hydra semantics.

This will cause any rooms using old/experiental versions unknown to
the js-sdk to break, but will mean that wehn the next room version
comes out, we'll use hydra for it which is, of course, not a given,
but is way more likely than going back to the old semantics.

The mobile Element clients currently hardcode hydra versions (ie.
as it is without this change, but we expect them to make this same
change soon after the hydra release.

We do NOT expect this to land with the hydra release, but target it for
the release after.

Reverts 1e5054a8ff87f83b0875916aa16f435853bf165a from https://github.com/matrix-org/matrix-js-sdk/pull/4937

See https://github.com/element-hq/element-meta/issues/2921 for public
discussion.
2025-08-26 10:49:44 +00:00
RiotRobot 77c118084b v38.0.0-rc.1 2025-08-21 14:16:23 +00:00
ElementRobot 097bfe451a Release tranche of breaking changes (#4963) (#4975)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2025-08-21 15:08:48 +01:00
Michael Telatynski b80d0091d2 Release tranche of breaking changes (#4963)
* Remove deprecated `IJoinRoomOpts.syncRoom` option (#4914)

This option is non-functional, and was deprecated in
https://github.com/matrix-org/matrix-js-sdk/pull/4913. Remove it altogether.

* Remove support for `onlyData != true` (#4939)

* Remove deprecated fields, methods, utilities (#4959)

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2025-08-21 13:24:36 +00:00
Timo 3a33c658bb Expose the StatusChanged event through the RTCSession (#4974)
* Expose the StatusChanged event through the RTCSession

Signed-off-by: Timo K <toger5@hotmail.de>

* add membershipManagerStatus public get field

Signed-off-by: Timo K <toger5@hotmail.de>

* add probably left as a getter

Signed-off-by: Timo K <toger5@hotmail.de>

* add tests for coverage

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-20 16:48:56 +00:00
Timo 81e42b9531 Add probablyLeft event to the MatrixRTCSession (#4962)
* Add probablyLeft emission to the MatrixRTCSession

Signed-off-by: Timo K <toger5@hotmail.de>

* add docstring

Signed-off-by: Timo K <toger5@hotmail.de>

* Review: add additional test + fix pending promises issue.

Signed-off-by: Timo K <toger5@hotmail.de>

* review: `Pick` only a subset of membership manager events

Signed-off-by: Timo K <toger5@hotmail.de>

* reveiw: update probablyLeft logic to be more straight forward

Signed-off-by: Timo K <toger5@hotmail.de>

* fix test

Signed-off-by: Timo K <toger5@hotmail.de>

* make test not wait for 5s

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* fix linter (rebase)

Signed-off-by: Timo K <toger5@hotmail.de>

* fix import

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-20 12:05:07 +00:00
renovate[bot] 7f7ecd060d Update dependency lint-staged to v16.1.5 (#4969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 15:06:35 +00:00
renovate[bot] 6126ee125a Update typedoc (#4970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-19 14:27:08 +00:00
renovate[bot] 78f718ff82 Update typescript-eslint monorepo to v8.39.1 (#4971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:27:09 +00:00
renovate[bot] c1c1be0c5d Update actions/download-artifact action to v5 (#4973)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:26:16 +00:00
renovate[bot] 1952eaa1ff Update actions/checkout action to v5 (#4972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:26:08 +00:00
renovate[bot] 8851f8b07c Update dependency @types/node to v18.19.123 (#4968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:51 +00:00
renovate[bot] 15eafe34b3 Update babel monorepo to v7.28.3 (#4966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:25 +00:00
renovate[bot] f0d48236fa Update dependency @stylistic/eslint-plugin to v5.2.3 (#4967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:23 +00:00
renovate[bot] dde9d48726 Update shogo82148/actions-upload-release-asset digest to 62365f2 (#4965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:06 +00:00
RiotRobot 6d046edcb2 v38.0.0-rc.0 2025-08-19 13:08:13 +00:00
Timo 2abf7ca795 Allow multiple rtc sessions per room (with different sessionDescriptions) (#4945)
* Introduce sessionDescription

Signed-off-by: Timo K <toger5@hotmail.de>

* Make sessionDescription part of a MatrixRTCSession

Signed-off-by: Timo K <toger5@hotmail.de>

* Make session manager only menage session for one sessionDescription

Signed-off-by: Timo K <toger5@hotmail.de>

* make membership manager aware about session (application + id)
Before this was just hardcoded to a call session

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests

Signed-off-by: Timo K <toger5@hotmail.de>

* fix doc comments

Signed-off-by: Timo K <toger5@hotmail.de>

* Make fields private,  improve comments, improve whitespace, don't use deprecated fields

Signed-off-by: Timo K <toger5@hotmail.de>

* add test for other application end event

Signed-off-by: Timo K <toger5@hotmail.de>

* rename call -> session

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests

Signed-off-by: Timo K <toger5@hotmail.de>

* remove id check since its already part of `deepCompare(membership.sessionDescription, sessionDescription)`

Signed-off-by: Timo K <toger5@hotmail.de>

* remove scope related tests. The id should be the only thing that scopes sessions. everything else is application (session type) specific

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* add test for custom sessionDescription

Signed-off-by: Timo K <toger5@hotmail.de>

* callMembershipsForRoom to default to call

Signed-off-by: Timo K <toger5@hotmail.de>

* roomSessionForRoom backwards compatible (And deprecate the call specific method)

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-19 11:58:57 +00:00
Olivier D 2b46579bd8 Add support for login_hint in authorization url generation (#4943)
* add login_hint to authorization url generation

Signed-off-by: olivier <odelcroi@gmail.com>

Signed-off-by: olivier <odelcroi@gmail.com>

fix lint

Signed-off-by: olivier <odelcroi@gmail.com>

* update doc

* fix linter

---------

Signed-off-by: olivier <odelcroi@gmail.com>
Co-authored-by: mcalinghee <mcalinghee.dev@gmail.com>
2025-08-18 15:06:59 +00:00
fkwp 6d42ed338e Only process MatrixRTC sessions associated with calls for callMembershipsForRoom (#4960)
* Only process MatrixRTC sessions associated with calls

* tests: Only process MatrixRTC sessions associated with calls

* linting
2025-08-14 12:44:41 +00:00
RiotRobot ef080c25f9 Merge branch 'master' into develop 2025-08-11 11:13:57 +00:00
RiotRobot c8d7b458b2 v37.13.0 2025-08-11 11:13:14 +00:00
ElementRobot f1ba8a8775 Support v12 rooms in maySendEvent (#4955) (#4956)
Follows on from https://github.com/matrix-org/matrix-js-sdk/pull/4937

(cherry picked from commit dea184e9ec)

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2025-08-08 15:17:12 +01:00
ElementRobot d21adf568a Support for creator power level (#4937) (#4954)
* Support for creator power level

Adds support for infinite power level specified by [MSC4289](https://github.com/matrix-org/matrix-spec-proposals/pull/4289).

* Update unit test

* Hardcode versions

as room versions strings aren't ordered

* Add test for v12 rooms

* Use more compact syntax



* Fix doc



* Fix additionalCreators from PR edit

* Split out hydra room version check

* Move power level logic into room state

Which already has knowledge of the room create event

* Add docs

* Fix unused bits

* Fix docs

* Fix lying docstring

* Reverse logic for hydra semantics

Assume unknown room versions do use hydra

* Use backticks



* Switch back to hardcoding just the two hydra versions

---------



(cherry picked from commit e119bf9040)

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-08-08 15:09:10 +01:00
David Baker dea184e9ec Support v12 rooms in maySendEvent (#4955)
Follows on from https://github.com/matrix-org/matrix-js-sdk/pull/4937
2025-08-08 13:19:51 +00:00
David Baker e119bf9040 Support for creator power level (#4937)
* Support for creator power level

Adds support for infinite power level specified by [MSC4289](https://github.com/matrix-org/matrix-spec-proposals/pull/4289).

* Update unit test

* Hardcode versions

as room versions strings aren't ordered

* Add test for v12 rooms

* Use more compact syntax

Co-authored-by: R Midhun Suresh <hi@midhun.dev>

* Fix doc

Co-authored-by: R Midhun Suresh <hi@midhun.dev>

* Fix additionalCreators from PR edit

* Split out hydra room version check

* Move power level logic into room state

Which already has knowledge of the room create event

* Add docs

* Fix unused bits

* Fix docs

* Fix lying docstring

* Reverse logic for hydra semantics

Assume unknown room versions do use hydra

* Use backticks

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Switch back to hardcoding just the two hydra versions

---------

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-08-08 10:23:49 +00:00
renovate[bot] c7f982e190 Update typedoc (#4949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 14:25:57 +00:00
renovate[bot] 2e2dd628c1 Update typescript-eslint monorepo to v8.39.0 (#4952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:56:03 +00:00
renovate[bot] 5ac5a8a799 Update dependency @babel/runtime to v7.28.2 (#4950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:55:49 +00:00
renovate[bot] 7d75ab417a Update dependency lint-staged to v16.1.4 (#4948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:40:14 +00:00
renovate[bot] 3ca81e409a Update dependency @types/node to v18.19.121 (#4947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:40:03 +00:00
RiotRobot 764fdb1d30 v37.13.0-rc.0 2025-08-05 12:51:02 +00:00
Timo c2d25d9377 Fix possible unknown state (With reproducible test) (#4944)
Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-01 09:13:35 +00:00
Richard van der Hoff c4e1e0723e Experimental support for sharing encrypted history on invite (#4920)
* tests: Cross-signing keys support in `E2EKeyReceiver`

Have `E2EKeyReceiver` collect uploaded cross-signing keys, so that they can be
returned by `E2EKeyResponder`.

* tests: Signature upload support in `E2EKeyReceiver`

Have `E2EKeyReceiver` collect uploaded device signatures, so that they can be
returned by `E2EKeyResponder`.

* tests: Implement `E2EOTKClaimResponder` class

A new test helper, which intercepts `/keys/claim`, allowing clients under test
to claim OTKs uploaded by other devices.

* Expose experimental settings for encrypted history sharing

Add options to `MatrixClient.invite` and `MatrixClient.joinRoom` to share and
accept encrypted history on invite, per MSC4268.

* Clarify pre-join-membership logic

* Improve tests

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

Co-authored-by: Hubert Chathi <hubertc@matrix.org>

---------

Co-authored-by: Hubert Chathi <hubertc@matrix.org>
2025-07-29 15:42:35 +00:00
RiotRobot 56b24c0bdc Merge branch 'master' into develop 2025-07-29 12:59:32 +00:00
RiotRobot aa7a709e3f v37.12.0 2025-07-29 12:58:45 +00:00
Valere Fedronic c57c47319e MatrixRTC: comply with the manageMediaKeys EncryptionConfig option (#4942)
* MatrixRTC: comply with the `manageMediaKeys` JoinConfig option

* add additional test for reception

* add comments about temporary solution
2025-07-28 15:11:29 +00:00
Richard van der Hoff 812d0aaef6 Use js-sdk logger in rust sdk (#4918) 2025-07-25 09:57:25 +00:00
Richard van der Hoff 61e07633df Update to matrix-sdk-crypto-wasm 15.1.0, and add new ShieldStateCode.MismatchedSender (#4916)
* test: add a flushPromises

this seems to be needed because `initRustCrypto` now ends up doing slightly
less awaiting

* Support new `ShieldStateCode.MismatchedSender`

* Update to matrix-sdk-crypto-wasm 15.1.0

* Add `waitFor` and use it instead of `flushPromises`

* minor lints and fixes

* another lint fix
2025-07-24 13:35:55 +00:00
Richard van der Hoff c7dbd6e33b fetch api: add support for downloading raw response (#4917)
* Factor out `BaseRequestOpts`

... to make it easier to find the docs from methods that use it.

* fetch api: add support for downloading raw response

I need to make an authenticated request to the media repo, and expect to get a
binary file back. AFAICT there is no easy way to do that right now.

* Clarify doc strings

* Various fixes
2025-07-24 11:06:52 +00:00
Richard van der Hoff 556494b8f0 Attempt deflaking of queueToDevice test (#4936) 2025-07-24 09:20:54 +00:00
renovate[bot] bf3b4e81b2 Update typescript-eslint monorepo to v8.37.0 (#4935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:36:31 +00:00
renovate[bot] 2710600389 Update all non-major dependencies (#4930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:12:13 +00:00
renovate[bot] ca168c494b Update dependency @types/node to v18.19.120 (#4931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:12:11 +00:00
renovate[bot] 759f5ed3eb Update dependency typedoc-plugin-mdn-links to v5.0.4 (#4932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 13:38:21 +00:00
renovate[bot] 53deedd2d6 Update dependency @stylistic/eslint-plugin to v5.2.0 (#4934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 13:37:49 +00:00
RiotRobot 4fbb9f92a7 v37.12.0-rc.0 2025-07-22 13:23:33 +00:00
Richard van der Hoff 1a58ce4649 Simplify declaration of ResponseType (#4926)
Currently, this is looking for a `json` property on `IHttpOpts`. There is no
such property, so that part of the declaration is completely redundant, and we
may as well remove it.

I looked into making it check `IRequestOpts`, which *does* have a `json`
property, but couldn't make it work.

Also add some docs, while we're there.
2025-07-22 10:07:56 +00:00
dependabot[bot] 32509d1fd1 Bump form-data from 4.0.0 to 4.0.4 (#4928)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 09:03:52 +00:00
Timo 11c9e39e5a Custom abort timeout logic for restarting delayed events that is compatible with the widget api (#4927)
* add custom local timout + add delay to 0 for normal local timeout.

* consider retry limits for new custom error

* mock the AbortError so we can reuse `actionUpdateFromErrors`

* update comment
2025-07-21 18:42:21 +00:00
toriningen 0ce944f3da Fix deep import incompatibility (#4924) (#4925) 2025-07-21 08:08:24 +00:00
Richard van der Hoff 38e04c8fb0 Give MatrixClient.invite an options param (#4919) 2025-07-19 07:29:12 +00:00
Valere Fedronic 1fcbc6ebeb RTCEncryptionManager: Joiner key rotation grace period (#4911)
* RTCEncryptionManager: Joiner key rotation grace period

* Test to clarify useKeyDelay and keyRotationGracePeriodMs interference

* make test more configurable

* rename delayRolloutTimeMillis to useKeyDelay same as config option

* rename skipRotationGracePeriod to keyRotationGracePeriodMs

* clarify that oldMemberships is not used by RTCEncryptionManager

* improve doc

* cleanup test

* more comment in test

* comment additions

* cleanup runOnlyPendingTimers

---------

Co-authored-by: Timo <toger5@hotmail.de>
2025-07-18 15:16:45 +00:00
Robin aa79236ce2 Allow sending notification events when starting a call (#4826)
* Make it easier to mock call memberships for specific user IDs

* Allow sending notification events when starting a call

* rename notify -> notification

* replace `joining` concept with `ownMembership`

* introduce new `m.rtc.notification` event alongside `m.call.notify`

* send new notification event alongside the deprecated one

* Test for new notification event type

* update relation string to match msc

* review

* fix doc errors

* fix tests + format

* remove anything decline related

---------

Co-authored-by: Timo <toger5@hotmail.de>
2025-07-18 12:42:57 +00:00
RiotRobot f8f1bf3837 Merge branch 'master' into develop 2025-07-15 14:45:35 +00:00
RiotRobot 946cf4f359 v37.11.0 2025-07-15 14:44:53 +00:00
Valere Fedronic c077201f2a EncryptionManager: un-deprecating EncryptionManager.getEncryptionKeys (#4912)
* EncryptionManager: should be able to re-emit keys

* fix typo in test file name

* review unneeded cast

* remove bad comment
2025-07-15 07:47:03 +00:00
Richard van der Hoff 53f2ad41d6 Deprecate non-functional IJoinRoomOpts.syncRoom (#4913)
I don't know when this last did something, but it's been a while.
2025-07-10 08:40:17 +00:00
Richard van der Hoff be15a709c6 Tests: gate logging behind DEBUG env var (#4903)
* Add `DebugLogger` type for logging matrix-js-sdk to `debug`

* unit tests for DebugLogger

* Use `DebugLogger` in some tests

* Use `DebugLogger` in rust-crypto.spec

* test-utils: silence some logging
2025-07-10 06:15:00 +00:00
renovate[bot] 090b8079db Update all non-major dependencies (#4905)
* Update all non-major dependencies

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-09 09:39:57 +00:00
Richard van der Hoff 06a1e1a88a More incorrect logger use (#4904)
A couple of places where we were still using the legacy logger
2025-07-08 15:37:17 +00:00
renovate[bot] 024b62bba0 Update dependency @stylistic/eslint-plugin to v5 (#4910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 14:36:47 +00:00
renovate[bot] 6bfb911cf7 Update dependency @types/node to v18.19.115 (#4906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 13:58:11 +00:00
renovate[bot] ad5da7cfa1 Update babel monorepo to v7.28.0 (#4909)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 13:38:22 +00:00
renovate[bot] e55bd1e14c Update typescript-eslint monorepo to v8.35.1 (#4908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 13:38:09 +00:00
renovate[bot] 119e859741 Update dependency typedoc to v0.28.7 (#4907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 13:37:59 +00:00
RiotRobot b7cce93edc v37.11.0-rc.0 2025-07-08 13:21:56 +00:00
Valere Fedronic e5c8c20a34 MatrixRTC: Refactor | Introduce a new Encryption manager (used with experimental to device transport) (#4799)
* refactor: New encryption manager BasicEncryptionManager for todevice

fixup: bad do not commit

* fix: ToDevice transport not setting the sent_ts

* test: BasicEncryptionManager add statistics tests

* code review

* feat: Encryption manager just reshare on new joiner

* refactor: Rename BasicEncryptionManger to RTCEncryptionManager

* fixup: RTC experimental todevice should use new encryption mgr

* fixup: use proper logger hierarchy

* fixup: RTC rollout first key asap even if no members to send to

* fixup: RTC add test for first key use

* fixup! emitting outbound key before anyone registered

* fix: quick patch for transport switch, need test

* test: RTC encryption manager, add test for transport switch

* post rebase fix

* Remove bad corepack commit

* review: cleaning, renaming

* review: cleaning and renaming

* stop using root logger in favor of a parent logger

* post merge fix broken test

* remove corepack again

* fix reverted changes after a merge

* review: Properly deprecate getEncryptionKeys

* review: rename ensureMediaKeyDistribution to ensureKeyDistribution

* review: use OutdatedKeyFilter instead of KeyBuffer
2025-07-08 12:43:16 +00:00
Neil Johnson 137379b7b7 Update README.md
style
2025-07-04 17:43:29 +01:00
Neil Johnson 4981efa54b Update README to make Element sponsorship explicit (#4901) 2025-07-04 17:41:35 +01:00
Richard van der Hoff 6e9740d787 Use client logger in more places (crypto code) (#4900)
* Use client logger for `RustBackupManager`

* use client logger in `CrossSigningIdentity`

* use client logger in `OutgoingRequestProcessor`

* RoomEncryptor: use correct logger for logDuration

use the logger for this specific event, rather than the more general one for the room

* Use client logger in `RoomEncryptor`
2025-07-04 07:35:42 +00:00
Richard van der Hoff 70257e0ab4 use client logger in MatrixRTCSessionManager (#4898) 2025-07-03 15:56:51 +00:00
Valere Fedronic 9baba151c6 Move ClientEvent docs to the event itself instead of the type map (#4894)
* doc: Add ClientEvent doc on the event enum

* Cleanup: Remove useless doc on EventHandlerMap

* quick format
2025-07-03 12:26:10 +00:00
Richard van der Hoff b4672e26ec Use client logger in more places (core code) (#4899)
* Use client logger for sync

Use the logger attached to the MatrixClient when writing log messages out of
the sync api. This helps figure out what's going on when multiple clients are
running in the same JS environment.

* Use client logger for to-device message queue

* Use client logger in `PushProcessor.rewriteDefaultRules`

* use client logger in `ServerCapabilities`

* Mark global `logger` as deprecated
2025-07-03 09:58:01 +01:00
Richard van der Hoff 940d358b0e Test: stop loading Olm into global namespace (#4895)
* Test: stop loading Olm into global namespace

Now that the js-sdk no longer relies on libolm, there is no need to populate
`globalThis.Olm`. Remove the code that did so (or relied on it being done).

* fix lint
2025-07-02 14:48:45 +00:00
Valere Fedronic 161c12f5d5 crypto: Add new ClientEvent.ReceivedToDeviceMessage with proper OlmEncryptionInfo support (#4891)
* crypto: Add new ClientEvent.ReceivedToDeviceMessage

refactor rename ProcessedToDeviceEvent to ReceivedToDeviceEvent

* fix: Restore legacy isEncrypted() for to-device messages

* Update test for new preprocessToDeviceMessages API

* quick fix on doc

* quick update docs and renaming

* review: Better doc and names for OlmEncryptionInfo

* review: Remove IToDeviceMessage alias and only keep IToDeviceEvent

* review: improve comments of processToDeviceMessages

* review: pass up encrypted event when no crypto callbacks

* review: use single payload for ReceivedToDeviceMessage

* fix linter

* review: minor comment update
2025-07-02 08:02:23 +00:00
RiotRobot de659d6431 Merge branch 'master' into develop 2025-07-01 14:54:29 +00:00
RiotRobot b095aa600d v37.10.0 2025-07-01 14:53:40 +00:00
David Baker 18ea8befdc Specify typeroot (#4892)
This prevents tsc from picking up random types from parent directories
such as in situations like an element-web layered build, and generally
seems like good hygiene as we don't want to pick up random types from
whatever directory we happen to be checked out into.
2025-07-01 11:31:28 +00:00
Timo 4f9ca2c697 Remove LegacyMembershipManager (#4862)
* Remove `LegacyMemberhsipManager`

* remove tests from rtc session
Those tests were only run with the legacy membership manager and are redundant with the memberhsip manager test spec.

* fix tests

* dont use non existing TestManager anymore

* remove fails for legacy

* fix another test
2025-06-26 12:19:51 +00:00
renovate[bot] 57a4dc8841 Update dependency @types/node to v18.19.112 (#4886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 12:34:38 +00:00
renovate[bot] 841c02e56d Update shogo82148/actions-upload-release-asset digest to 610b198 (#4885)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 11:04:54 +00:00
renovate[bot] 6ece4c3c16 Update all non-major dependencies (#4888)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 16:44:15 +00:00
renovate[bot] 5a3c07f91d Update typescript-eslint monorepo to v8.34.1 (#4887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 15:54:48 +00:00
renovate[bot] 3ed4b3ed50 Update mheap/github-action-required-labels digest to 8afbe8a (#4884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 15:20:47 +00:00
renovate[bot] 9ca9bd9baf Update guibranco/github-status-action-v2 digest to 741ea90 (#4883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 15:02:53 +00:00
RiotRobot 0265f6ea2d v37.10.0-rc.0 2025-06-24 12:40:23 +00:00
Richard van der Hoff 8f597f0f87 Update matrix-sdk-crypto-wasm to 15.0.0 (#4882)
For js-sdk users, this includes the following:

    -   Send stable identifier `sender_device_keys` for MSC4147 (Including device keys with Olm-encrypted events).
        ([#4964](https://github.com/matrix-org/matrix-rust-sdk/pull/4964))

    -   Check the `sender_device_keys` field on _all_ incoming Olm-encrypted to-device messages and ignore any to-device messages which include the field but whose data is invalid (as per [MSC4147](https://github.com/matrix-org/matrix-spec-proposals/pull/4147)).
        ([#4922](https://github.com/matrix-org/matrix-rust-sdk/pull/4922))

    -   Fix bug which caused room keys to be unnecessarily rotated on every send in the presence of blacklisted/withheld devices in the room.
        ([#4954](https://github.com/matrix-org/matrix-rust-sdk/pull/4954))

    -   Fix [matrix-rust-sdk#2729](https://github.com/matrix-org/matrix-rust-sdk/issues/2729) which in rare cases can cause room key oversharing.
        ([#4975](https://github.com/matrix-org/matrix-rust-sdk/pull/4975))
2025-06-23 17:35:37 +00:00
Michael Telatynski 67df2a53c4 Remove redundant git-revision.txt file (#4881)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-06-20 08:08:30 +00:00
Patrick Cloke 8367277894 Allow customizing the IndexedDB database prefix used by Rust crypto. (#4878)
* Allow customizing the IndexedDB database prefix used by Rust crypto.

Related to #3974

Signed-off-by: Patrick Cloke <clokep@patrick.cloke.us>

* Rename argument

---------

Signed-off-by: Patrick Cloke <clokep@patrick.cloke.us>
2025-06-18 09:07:35 +00:00
RiotRobot 4efb27354f Merge branch 'master' into develop 2025-06-17 13:09:40 +00:00
RiotRobot 28bc90563e v37.9.0 2025-06-17 13:09:06 +00:00
Will Hunt f9be1bf57a Add prepare script (#4877)
* Add prepare script

* Remove 'prepack' script.
2025-06-17 10:18:19 +00:00
Richard van der Hoff adaf921623 Remove @matrix-org/olm from dependency list (#4876)
Olm is needed at build time to run the tests, but is no longer needed at
runtime.
2025-06-12 09:28:39 +00:00
Travis Ralston cdece6cb9f Redact on ban: Client implementation (#4867)
* First pass implementation

* fix naming/docs

* apply lint

* Add test for existing behaviour

* Add happy path tests

* Fix bug identified by tests

* ... and this is why we add negative tests too

* Add some sanity tests

* Apply linter
2025-06-10 14:28:02 +00:00
renovate[bot] d438e25f87 Update dependency @stylistic/eslint-plugin to v4.4.1 (#4873)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 14:16:20 +00:00
renovate[bot] 73d8f4384d Update all non-major dependencies (#4870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 13:56:32 +00:00
renovate[bot] b0cb6aa724 Update babel monorepo (#4871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 13:56:20 +00:00
renovate[bot] dfc26f8aa1 Update definitelyTyped (#4872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 13:56:11 +00:00
renovate[bot] 286fb8f752 Update typescript-eslint monorepo to v8.33.1 (#4874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 13:55:42 +00:00
RiotRobot 921de6807d v37.9.0-rc.0 2025-06-10 13:16:01 +00:00
RiotRobot d4e7b949e7 Merge branch 'master' into develop 2025-06-10 12:00:47 +00:00
RiotRobot 40bc833bb7 v37.8.0 2025-06-10 12:00:13 +00:00
ElementRobot a46ad75440 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.2.1 (#4868) (#4869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 12:52:35 +01:00
renovate[bot] 1e80538cfb Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.2.1 (#4868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 11:30:04 +00:00
Michael Telatynski 73cbcfa4ee Ensure we send spec-compliant filter strings by stripping out null values (#4865)
* Ensure we send spec-compliant filter strings by stripping out null values

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-06-09 08:32:42 +00:00
Richard van der Hoff 99972ce0a9 Factor out common code for processing to-device events (#4863)
`sliding-sync-sdk.ts` and `sync.ts` both have copies of this code, and it's
redundant.
2025-06-05 08:57:50 +00:00
Timo 44399f6017 Fix MatrixRTC membership manager failing to rejoin in a race condition (sync vs not found response) (#4861)
* add test run helper to allow running long tests in vs code

* deprecate IDeferred (as its associated defer method is also deprecated and its just a type rename to PromiseWithResolvers)

* Improve docs and readability of MembershipManager.spec.ts

* Intoduce test for a race condition which results in a state where the state event and the hasMemberStateEvent variable diverge

* fix room state and membership manager state diverging. See:
https://github.com/element-hq/element-call-rageshakes/issues/10609
https://github.com/element-hq/element-call-rageshakes/issues/10594
https://github.com/element-hq/element-call-rageshakes/issues/9902

* logging, docstings and variable name improvements

* review

* review pending timers
2025-06-04 10:44:12 +00:00
RiotRobot c387f30e5c Merge branch 'master' into develop 2025-06-03 14:50:36 +00:00
RiotRobot eb9867a5ba v37.7.0 2025-06-03 14:50:05 +00:00
rsb-tbg 12a9875c46 Include extraParams in all HTTP requests (#4860)
* attaching queryParams from client config in getUrl

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>

* changed client queryParams to QueryDict for consistency and now merging both sets of params in getUrl if one or both exist

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>

* added tests

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>

---------

Signed-off-by: rsb-tbg <69879226+rsb-tbg@users.noreply.github.com>
2025-05-30 09:09:21 +00:00
renovate[bot] 74f5efc4ef Update all non-major dependencies (#4859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-29 08:01:03 +00:00
renovate[bot] 43d47982ed Update dependency @types/node to v18.19.105 (#4858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-29 07:47:37 +00:00
renovate[bot] 94fb489952 Update dependency @stylistic/eslint-plugin to v4.4.0 (#4855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 15:36:54 +00:00
renovate[bot] 75ae05e5eb Update typescript-eslint monorepo to v8.33.0 (#4856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:45:13 +00:00
renovate[bot] 9058b79c39 Update all non-major dependencies (#4854)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:44:33 +00:00
renovate[bot] 671dd2ca40 Update dependency typedoc to v0.28.5 (#4852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:44:13 +00:00
renovate[bot] 34f35393ff Update dependency @types/node to v18.19.103 (#4851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:43:50 +00:00
renovate[bot] e206a12902 Update babel monorepo to v7.27.3 (#4850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:43:26 +00:00
RiotRobot 76eabb2efa v37.7.0-rc.0 2025-05-28 13:02:09 +00:00
Robin bf6dc16ad3 Allow the embedded client to work without update_state support (#4849)
* Allow the embedded client to work without UpdateState version

* Test that RoomWidgetClient can receive state without update_state

* add sliding sync test

* sliding sync receive test

* review

* add doc comment

---------

Co-authored-by: Timo <toger5@hotmail.de>
2025-05-23 15:09:52 +00:00
Timo 9398271695 Check for unknown variant on to-device sending and fall back to room event encryption. (#4847)
* Check for `unknown variant` on to-device sending and fallback to room event encryption.

* fix tests

* fix error js-sdk api type

* Change logger from debug to warn for unsupported to-device transport and improve error message comments

* also add case for not supported
This will be send by the driver in case we sent an encrypted to-device but do not have support of that.

---------

Co-authored-by: Robin <robin@robin.town>
2025-05-22 13:43:47 +00:00
Andy Balaam ef7a818f70 Bump matrix-sdk-crypto-wasm to 14.2.0 (#4848)
For better logging when a backed up key fails to deserialise.
2025-05-22 12:03:55 +00:00
Robin b8903ddf3e Reapply "Distinguish room state and timeline events in embedded clients" (#4790)
This reverts commit fd9a44e701.

We are ready to reintroduce support for the `update_state` widget action (https://github.com/matrix-org/matrix-spec-proposals/pull/4237) now that matrix-rust-sdk is about to gain support for it as well.
2025-05-20 16:03:24 +00:00
RiotRobot c35c7d1a3b Merge branch 'master' into develop 2025-05-20 13:27:51 +00:00
RiotRobot 286e00c500 v37.6.0 2025-05-20 13:27:14 +00:00
renovate[bot] 26922a61f3 Update dependency debug to v4.4.1 (#4846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-14 14:52:36 +00:00
Timo 457a300c95 MatrixRTC: Rename MembershipConfig parameters (#4714)
* Remove redundant sendDelayedEventAction
We do already have the state `hasMemberEvent` that allows to distinguish the two cases. No need to create two dedicated actions.

* fix missing return

* Make membership manager an event emitter to inform about status updates.
 - deprecate isJoined (replaced by isActivated)
 - move Interface types to types.ts

* add tests for status updates.

* lint

* test "reschedules delayed leave event" in case the delayed event gets canceled

* review

* fix types

* prettier

* fix legacy membership manager

* remove deprecated jitter.

* use non deprecated config fields (keep deprecated fields as fallback)

* update tests to test non deprecated names

* make local NewMembershipManager variable names consistent with config

* make LegacyMembershipManger local variables consistent with config

* comments and rename `networkErrorLocalRetryMs` -> `networkErrorRetryMs`

* review
2025-05-13 20:15:41 +00:00
Michael Telatynski be04f003ce Remove @types/uuid - uuid has its own types now (#4845)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-05-13 18:07:22 +00:00
dependabot[bot] 67b445d40d Bump base-x from 5.0.0 to 5.0.1 (#4844)
Bumps [base-x](https://github.com/cryptocoinjs/base-x) from 5.0.0 to 5.0.1.
- [Commits](https://github.com/cryptocoinjs/base-x/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: base-x
  dependency-version: 5.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 12:51:33 +00:00
renovate[bot] 27f28d5558 Pin dependencies (#4843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 12:13:04 +00:00
renovate[bot] 52bac9648b Update dependency typedoc-plugin-mdn-links to v5.0.2 (#4842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 12:11:22 +00:00
renovate[bot] 7e0e5a3243 Update all non-major dependencies (#4836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 11:49:55 +00:00
renovate[bot] 337d1791cf Update babel monorepo (#4837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 11:00:33 +00:00
renovate[bot] 622998e949 Update dependency @types/node to v18.19.100 (#4838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 11:00:13 +00:00
renovate[bot] 673806ecaa Update dependency typedoc to v0.28.4 (#4839)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 10:59:41 +00:00
renovate[bot] b2232289a6 Update typescript-eslint monorepo to v8.32.0 (#4840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 10:59:26 +00:00
renovate[bot] 344c8fad9d Update dependency lint-staged to v16 (#4841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 10:59:18 +00:00
RiotRobot cccaaf6e56 v37.6.0-rc.0 2025-05-13 10:54:01 +00:00
Michael Telatynski 4dbca983b4 Fix autodiscovery handling of 2xx (non-200) codes (#4833)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-05-12 18:51:30 +00:00
Michael Telatynski 54e8f3c9d0 Simplify global types (#4831)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-05-12 09:02:49 +00:00
Michael Telatynski 1fcc375dd5 Deprecate utils function defer in favour of Promise.withResolvers (#4829)
* Switch from defer to Promise.withResolvers

As supported by the outgoing LTS version (v22) which has 99% support of ES2024

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Deprecate defer instead of killing it

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Knip

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate based on review

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-05-09 10:16:35 +00:00
Michael Telatynski d24c5d8b2b Update to Node 22 LTS (#4832)
Switch typescript lib to es2024 and make necessary type changes

Fixes https://github.com/matrix-org/matrix-js-sdk/issues/4796

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-05-08 13:38:18 +00:00
Richard van der Hoff bb9280ad6b Write a log line when cancelling verification (#4828)
... in an attempt to figure out what might have caused a spurious cancellation
today
2025-05-08 09:55:22 +00:00
RiotRobot 00bd7f0f02 Merge branch 'master' into develop 2025-05-06 13:48:19 +00:00
RiotRobot a29b8736f3 v37.5.0 2025-05-06 13:47:50 +00:00
ElementRobot 1c8a1cd5a1 Fix token refresh behaviour for non-expired tokens (#4825) (#4827)
The condition was inverted here, but the tests were passing because
they didn't add enough expiry time for the token expiry to be over
the threshold.

Fix the condition and tests, add another test and generally add a
bunch of comments so hopefully this is less confusing for the next
person.

Fixes https://github.com/element-hq/element-web/issues/29858

(cherry picked from commit fea619d34c)

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2025-05-06 14:20:24 +01:00
David Baker fea619d34c Fix token refresh behaviour for non-expired tokens (#4825)
The condition was inverted here, but the tests were passing because
they didn't add enough expiry time for the token expiry to be over
the threshold.

Fix the condition and tests, add another test and generally add a
bunch of comments so hopefully this is less confusing for the next
person.

Fixes https://github.com/element-hq/element-web/issues/29858
2025-05-06 09:39:13 +00:00
renovate[bot] f322f32a07 Update eslint (#4786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 11:36:13 +00:00
renovate[bot] bc3abd4394 Update dependency typedoc-plugin-coverage to v4 (#4824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 10:48:34 +00:00
renovate[bot] 6721aca293 Update typescript-eslint monorepo to v8.31.0 (#4823)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 10:47:48 +00:00
renovate[bot] 80b51c0e30 Update dependency typedoc to v0.28.3 (#4822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 10:47:35 +00:00
renovate[bot] 91aa1dc092 Update dependency @types/node to v18.19.87 (#4821)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 10:47:22 +00:00
renovate[bot] 78972e81d1 Update all non-major dependencies (#4820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 10:47:09 +00:00
RiotRobot 663136a95a v37.5.0-rc.0 2025-04-29 10:42:21 +00:00
Michael Telatynski d67b19fa88 Refactor how token refreshing works to be more resilient (#4819)
* Refactor how token refreshing works to be more resilient

1. ensure we do use the new token if it is not explicitly inhibited by the caller
2. eagerly refresh token if we know it is expired
3. allow refreshing a token multiple times if e.g. on bad connection or the environment has been slept and sufficient time has passed since the last refresh attempt

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add exponential backoff

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Ensure no timing effects on `authedRequest` method call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-04-29 08:13:27 +00:00
Johannes Marbach 6ec200adcf Stabilise MSC3765 (#4767)
* Stabilise MSC3765

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Remove unstable content and hardcode property name

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-04-25 12:35:18 +00:00
Timo 19b1b901f5 Inherit methodFactory extensions from the parent to the child loggers. (#4809)
* use methodFactory extensions from the rootLogger in child loggers.

* use simple method factory copy AND `childLogger.setLevel(childLogger.getLevel());`
This is the important part that actually registers the new methods.

* add comments and  find a way to make it clearer that the types are correct.

* review

* additionally fix MatrixRTCSessionManager being initialized before the extension is in place.

* Add comment to clarify order of log extensions and creating childs.

* review

* Set "loglevel" min version to guarantee access to `logger.rebuild`
2025-04-22 16:27:18 +00:00
RiotRobot 1f52fa0c43 Merge branch 'master' into develop 2025-04-22 12:52:54 +00:00
RiotRobot b08c083c46 v37.4.0 2025-04-22 12:52:26 +00:00
Richard van der Hoff 69f1bea89b Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.1.0 (#4811)
* Convert import/export room key tests to snapshots

* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.1.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 13:51:38 +00:00
renovate[bot] a52c64b747 Update typescript-eslint monorepo to v8.29.1 (#4806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 14:46:11 +00:00
renovate[bot] de9eb0e7f2 Update all non-major dependencies (#4802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 14:33:53 +00:00
renovate[bot] a97d7f1133 Update guibranco/github-status-action-v2 digest to 5f2b01c (#4801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 14:19:39 +00:00
renovate[bot] bc399abf6e Update dependency typescript to v5.8.3 (#4805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 14:14:39 +00:00
renovate[bot] 1085a2469a Update dependency typedoc to v0.28.2 (#4804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 14:14:02 +00:00
renovate[bot] 52a9c52b12 Update dependency @types/node to v18.19.86 (#4803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 14:13:28 +00:00
RiotRobot ac49137c0e v37.4.0-rc.0 2025-04-15 13:21:07 +00:00
Michael Telatynski a179227bf1 Revert "Load gpg key without external action (#4784)" (#4800)
This reverts commit 634651859b.
2025-04-15 12:51:18 +00:00
Timo 64e27f5d3c MatrixRTC: Add combined toDeviceAndRoomKeyTransport (#4792)
* Add to-device and room transport

* Lint

* add doc string

* hook up automatic toDeviceKeyTransport -> roomKeyTransport switching

* lint, rename, imports

* fix logging

* fix test logger

* use mockLogger better in tests

* improve logging and reduce `EnabledTransportsChanged` emission.

* fix this binding

* lint

* simplify `onTransportChanged` callback

* refactor to construct the transports outside the RoomAndToDeviceKeyTransport

* update tests to use new RoomAndToDeiviceTransport constructor

* add depractaion comments
2025-04-14 15:25:30 +00:00
Michael Telatynski 634651859b Load gpg key without external action (#4784)
* Load gpg key ourselves

* Update release-make.yml
2025-04-14 11:25:10 +00:00
Michael Telatynski 480c8e86a4 Fix token refresh racing with other requests and not using new token (#4798)
* Fix token refresh racing with other requests and not using new token

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-04-14 09:11:55 +00:00
Michael Telatynski 1ba4412260 Fix fallback to MemoryCryptoStore when LocalStorage unavailable (#4797)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-04-11 09:27:09 +00:00
Timo e3a3a52f2a Make logging consistent for matrixRTC (#4788)
* Consistent RTC logging

* tests: Add more RTC key transport tests

* test: improve rtc key room transport test

* fixup: missing mock

* rtc: more tests

* coverage trick

---------

Co-authored-by: Valere <bill.carson@valrsoft.com>
2025-04-10 15:51:16 +00:00
Valere Fedronic 3f03c1da89 MatrixRTC: ToDevice distribution for media stream keys (#4785)
* MatrixRTC: ToDevice distribution for media stream keys

* test: Add RTC to device transport test

* lint

* fix key indexing

* fix indexing take two
 - use correct value for: `onEncryptionKeysChanged`
 - only update `latestGeneratedKeyIndex` for "this user" key

* test: add test for join config `useExperimentalToDeviceTransport`

* update test to fail without the fixed encryption key index

* review

* review (dave)

---------

Co-authored-by: Timo <toger5@hotmail.de>
2025-04-10 08:28:01 +00:00
RiotRobot eb793aaa08 Merge branch 'master' into develop 2025-04-08 12:38:12 +00:00
RiotRobot f7bab544a7 v37.3.0 2025-04-08 12:37:43 +00:00
Florian Duros b3fd92ad16 fix(crypto): remove duplicate deleteSecretStorage in RustCrypto.resetEncryption (#4789) 2025-04-08 08:04:05 +00:00
Valere Fedronic ba71235539 MatrixRTC: Introduce key transport abstraction as prep work for to-device encryption (#4773)
* refactor: extract RoomKeyTransport class for key distribution

* refact: Call key transport, pass the target recipients to sendKey

* update IKeyTransport interface to event emitter.

* fix not subscribing to KeyTransportEvents in the EncryptionManager + cleanup

* fix one test and broken bits needed for the test (mostly statistics wrangling)

* fix tests

* add back decryptEventIfNeeded

* move and fix room transport tests

* dedupe isMyMembership

* move type declarations around to be at more reasonable places

* remove deprecated `onMembershipUpdate`

* fix imports

* only start keytransport when session is joined

* use makeKey to reduce test loc

* fix todo comment -> note comment

---------

Co-authored-by: Timo <toger5@hotmail.de>
2025-04-07 08:30:10 +00:00
David Baker d6ede767c9 Upgrade eslint, babel & related deps (#4783)
As they aren't passing the tests upgraded individually
2025-04-02 12:01:43 +00:00
renovate[bot] 43f388d89a Update dependency @types/node to v18.19.84 (#4778)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 15:40:35 +00:00
renovate[bot] 1d46b182c6 Update all non-major dependencies (#4777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 15:10:24 +00:00
renovate[bot] 352f6986cd Update crazy-max/ghaction-import-gpg digest to e89d409 (#4774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 14:28:41 +00:00
renovate[bot] 0ca72ec466 Update shogo82148/actions-upload-release-asset digest to d22998f (#4776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 14:28:28 +00:00
renovate[bot] 375f086f44 Update guibranco/github-status-action-v2 digest to 9b1d102 (#4775)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 14:27:53 +00:00
Florian Duros 5976083e32 Fix RustCrypto.resetEncryption failure (#4772)
* fix(crypto): add missing await to `resetEncryption`

* test(crypto): add `mockResolvedValue` to async mock of 4S
2025-04-01 12:58:41 +00:00
RiotRobot d53d2dff45 v37.3.0-rc.0 2025-04-01 12:28:09 +00:00
renovate[bot] 3657eb61d8 Update dependency matrix-widget-api to v1.13.1 (#4689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 12:52:27 +00:00
RiotRobot 1def88eb35 Merge branch 'master' into develop 2025-03-25 14:42:58 +00:00
RiotRobot 6f9e68c8f3 v37.2.0 2025-03-25 14:42:20 +00:00
Timo 5a65c8436d MatrixRTC MembershipManger: remove redundant sendDelayedEventAction and expose status (#4747)
* Remove redundant sendDelayedEventAction
We do already have the state `hasMemberEvent` that allows to distinguish the two cases. No need to create two dedicated actions.

* fix missing return

* Make membership manager an event emitter to inform about status updates.
 - deprecate isJoined (replaced by isActivated)
 - move Interface types to types.ts

* add tests for status updates.

* lint

* test "reschedules delayed leave event" in case the delayed event gets canceled

* review

* fix types

* prettier

* fix legacy membership manager
2025-03-25 12:49:47 +00:00
Michael Telatynski 2090319bdd Abstract logout-causing error type from tokenRefreshFunction calls (#4765)
* Abstract logout-causing error type from tokenRefreshFunction calls

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-25 10:31:58 +00:00
renovate[bot] f4de8837fd Update dependency typedoc to ^0.28.0 (#4759)
* Update dependency typedoc to ^0.28.0

* Upgrade typedoc-plugin-missing-exports for compatibility

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Upgrade typedoc-plugin-missing-exports for compatibility

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Bump typedoc

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-21 13:36:35 +00:00
Michael Telatynski 1e92c13a75 Improve PushProcessor::getPushRuleGlobRegex (#4764)
* Improve PushProcessor::getPushRuleGlobRegex

Fix cache key not taking non-pattern parameters into account
Use lookarounds to ensure the word boundary isn't treated as part of the match

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-21 10:57:14 +00:00
Michael Telatynski 8061fa924d Export push processor & method for converting matrix glob to regexp (#4763)
* Export push processor method for converting matrix glob to regexp

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Export pushProcessor from MatrixClient

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add capturing group around pattern match

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve comment

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-21 09:11:04 +00:00
m004 0760c5f64c Add authenticated media parameter to getMediaConfig (#4762)
* feat(client): Add authenticated media parameter to getMediaConfig

* test(client): add tests for mediaconfig

---------

Co-authored-by: Maurizio <maurizio-noah.abbruzzo-santiago@t-systems.com>
2025-03-20 16:16:11 +00:00
Richard van der Hoff e2bdb2f057 Rust crypto: set a timeout on outgoing HTTP requests (#4761) 2025-03-19 16:37:52 +00:00
David Baker fd47a189e0 Switch sliding sync support to simplified sliding sync (#4400)
* Switch sliding sync support to simplified sliding sync

Experimental PR to test js-sdk with simlified sliding sync.

This does not maintain support for regulaer sliding sync.

* Remove txn_id handling, ensure we always resend when req params change

* Fix some tests

* Fix remaining tests

* Mark TODOs on tests which need to die

* Linting

* Make comments lie less

* void

* Always sent full extension request

* Fix test

* Remove usage of deprecated field

* Hopefully fix DM names

* Refactor how heroes are handled in Room

* Fix how heroes work

* Linting

* Ensure that when SSS omits heroes we don't forget we had heroes

Otherwise when the room next appears the name/avatar reset to
'Empty Room' with no avatar.

* Check the right flag when doing timeline trickling

* Also change when the backpagination token is set

* Remove list ops and server-provided sort positions

SSS doesn't have them.

* Linting

* Add Room.bumpStamp

* Update crypto wasm lib

For new functions

* Add performance logging

* Fix breaking change in crypto wasm v8

* Update crypto wasm for breaking changes

See https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/releases/tag/v8.0.0
for how this was mapped from the previous API.

* Mark all tracked users as dirty on expired SSS connections

See https://github.com/matrix-org/matrix-rust-sdk/pull/3965 for
more information. Requires `Extension.onRequest` to be `async`.

* add ts extension

* Fix typedoc ref

* Add method to interface

* Don't force membership to invite

The membership was set correctly from the stripped state anyway so
this was redundant and was breaking rooms where we'd knocked.

* Missed merge

* Type import

* Make coverage happier

* More test coverage

* Grammar & formatting

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Remove markAllTrackedUsersAsDirty from crypto API

Not sure why this was in there, seems like it just needed to be in
crypto sync callbacks, which it already was.

* Remove I from interface

* API doc

* Move Hero definition to room-summary

* make comment more specific

* Move internal details into room.ts

and make the comment a proper tsdoc comment

* Use terser arrow function syntax

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Move comment to where we do the lookup

* Clarify comment

also prettier says hi

* Add comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Add tsdoc

explaining that the summary event will be modified

* more comment

* Remove unrelated changes

* Add docs & make fields optional

* Type import

* Clarify sync versions

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Make tsdoc comment & add info on when it's used.

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Rephrase comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Prettier

* Only fetch member for hero in legacy sync mode

* Split out a separate method to set SSS room summary

Rather than trying to fudge up an object that looked enough like the
old one that we could pass it in.

* Type import

* Make link work

* Nope, linter treats it as an unused import

* Add link the other way

* Add more detail to doc

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Remove unnecessary cast

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Remove length > 0 check

as it wasn't really necessary and may cause heroes not to be cleared?

* Doc params

* Remove unnecessary undefined comparison

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Put the comparison back

as it's necessary to stop typescript complaining

* Fix comment

* Fix comment

---------

Co-authored-by: Kegan Dougal <7190048+kegsay@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-03-18 17:23:45 +00:00
renovate[bot] c233334f27 Update all non-major dependencies (#4758)
* Update all non-major dependencies

* Hold back eslint-plugin-matrix-org

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-18 14:20:50 +00:00
renovate[bot] 029ab5f109 Update babel monorepo to v7.26.10 (#4755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 13:59:37 +00:00
renovate[bot] 366f686827 Update guibranco/github-status-action-v2 digest to fe98467 (#4754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 13:53:17 +00:00
renovate[bot] 26387287af Update typescript-eslint monorepo to v8.26.1 (#4757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 13:47:00 +00:00
renovate[bot] ba0bbe1cba Update dependency eslint-import-resolver-typescript to v4 (#4760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 13:43:41 +00:00
renovate[bot] 3c37d7b0fb Update dependency @types/node to v18.19.80 (#4756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 13:42:40 +00:00
RiotRobot 097d645c19 v37.2.0-rc.0 2025-03-18 13:24:18 +00:00
Will Hunt e62aabccd9 Add reportRoom API (#4753)
* Add reportRoom

* add test

* fixdoc

* fix doc again

* Add docs for matrix version.

* lint
2025-03-17 15:03:54 +00:00
Michael Telatynski 0f3bcf3736 Allow port differing in OIDC dynamic registration URIs (#4749)
As per discussion and amendment to https://github.com/matrix-org/matrix-spec-proposals/pull/2966

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-17 08:39:00 +00:00
Timo 8395919f0f MatrixRTC: Fix running not representing what we need from isJoined in EC (#4752)
* Fix running != isJoined
EC expects isJoined to represent if we should be in joined state or not. It does not correlate to what our actual state of the scheduler is. We used the scheduler running state before but on leave the running state will stay true until we successfully updated the room state.
EC expects isJoined to immediately be false.

This introduces a member variable `activated` that represents if the MemberhsipManager is trying to connect or trying to disconnect independent on the current state.

* simplify catch finally blocks
2025-03-13 17:36:22 +00:00
renovate[bot] 6cdc68d19d Update dependency @babel/runtime to v7.26.10 [SECURITY] (#4751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 16:24:04 +00:00
Michael Telatynski c879258545 Fix package.json repository.url (#4750)
Using `npm pkg fix`

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-13 16:04:58 +00:00
Michael Telatynski b14cc82682 OIDC: only pass logo_uri, policy_uri, tos_uri if they conform to "common base" (#4748)
* OIDC: only pass logo_uri, policy_uri, tos_uri if they conform to "common base"

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-13 14:47:09 +00:00
Timo 9f9be701e7 MatrixRTC: New membership manager (#4726)
* WIP doodles on MembershipManager test cases

* .

* initial membership manager test setup.

* Updates from discussion

* revert renaming comments

* remove unused import

* fix leave delayed event resend test.
It was missing a flush.

* comment out and remove unused variables

* es lint

* use jsdom instead of node test environment

* remove unused variables

* remove unused export

* temp

* review

* fixup tests

* more review

* remove wait for expect dependency

* temp

* fix wrong mocked meberhsip template

* rename MembershipManager -> LegacyMembershipManager
And remove the IMembershipManager from it

* Add new memberhsip manager

* fix tests to be compatible with old and new membership manager

* Comment cleanup

* Allow join to throw
 - Add tests for throwing cases
 - Fixs based on tests

* introduce membershipExpiryTimeoutSlack

* more detailed comments and cleanup

* warn if slack is misconfigured and use default values instead

* fix action resets.

* flatten MembershipManager.spec.ts

* rename testEnvironment to memberManagerTestEnvironment

* allow configuring Legacy manager in the matrixRTC session

* deprecate LegacyMembershipManager

* remove usage of waitForExpect

* flatten tests and add comments

* clean up leave logic branch

* add more leave test cases

* use defer

* review ("Some minor tidying things for now.")

* add onError for join method and cleanup

* use pop instead of filter

* fixes

* simplify error handling and MembershipAction
Only use one membership action enum

* Add diagram

* fix new error api in rtc session

* fix up retry counter

* fix lints

* make unrecoverable errors more explicit

* fix tests

* Allow multiple retries on the rtc state event http requests.

* use then catch for startup

* no try catch 1

* update expire headroom logic
transition from try catch to .then .catch

* replace flushPromise with advanceTimersByTimeAsync

* fix leaving special cases

* more unrecoverable errors special cases

* move to MatrixRTCSessionManager logger

* add state reset and add another unhandleable error
The error occurs if we want to cancel the delayed event we still have an id for but get a non expected error.

* missed review fixes

* remove @jest/environment dependency

* Cleanup awaits and Make mock types more correct.
Make every mock return a Promise if the real implementation does return a pormise.

* remove flush promise dependency

* fix not recreating default state on reset
This broke all tests since we only created the state once and than passed by ref

* Use per action rate limit and retry counter
There can be multiple retries at once so we need to store counters per action
e.g. the send update membership and the restart delayed could be rate limited at the same time.

* add linting to matrixrtc tests

* Add fix async lints and use matrix rtc logger for test environment.

* prettier

* review step 1

* change to MatrixRTCSession logger

* review step 2

* make LoopHandler Private

* update config to use NewManager wording

* emit error on rtc session if the membership manager encounters one

* network error and throw refactor

* make accessing the full room deprecated

* remove deprecated usage of full room

* Clean up the deprecation

* add network error handler and cleanup

* better logging, another test, make maximumNetworkErrorRetryCount configurable

* more logging & refactor leave promise

* add ConnectionError as possible retry cause

* Make it work in embedded mode with a server that does not support delayed events

* review iteration 1

* review iteration 2

* first step in improving widget error handling

* make the embedded client throw ConnectionErrors where desired.

* fix tests

* delayed event sending widget mode stop gap fix.

* improve comment

* fix unrecoverable error joinState (and add JoinStateChanged) emission.

* check that we do not add multipe sendFirstDelayed Events

* also check insertions queue

* always log "Missing own membership: force re-join"

* Do not update the membership if we are in any (a later) state of sending our own state.
The scheduled states MembershipActionType.SendFirstDelayedEvent and MembershipActionType.SendJoinEvent both imply that we are already trying to send our own membership state event.

* make leave reset actually stop the manager.
The reset case was not covered properly. There are cases where it is not allowed to add additional events after a reset and cases where we want to add more events after the reset. We need to allow this as a reset property.

* fix tests (and implementation)

* Allow MembershipManger to be set at runtime via JoinConfig.membershipManagerFactory

* Map actions into status as a sanity check

* Log status change after applying actions

* Add todo

* Cleanup

* Log transition from earlier status

* remove redundant status implementation
also add TODO comment to not forget about this.

* More cleanup

* Consider insertions in status()

* Log duration for emitting MatrixRTCSessionEvent.MembershipsChanged

* add another valid condition for connected

* some TODO cleanup

* review add warning when using addAction while the scheduler is not running.

* es lint

* refactor to return based handler approach (remove insertions array)

* refactor: Move action scheduler

* refactor: move different handler cases into separate functions

* linter

* review: delayed events endpoint error

* review

* Suggestions from pair review

* resetState is actually only used internally

* Revert "resetState is actually only used internally"

This reverts commit 6af4730919ec07ce9aaad8de35c27ac6b98a3019.

* refactor: running is part of the scheduler (not state)

* refactor: move everything state related from schduler to manager.

* review

* Update src/matrixrtc/NewMembershipManager.ts

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>

* review

* public -> private + missed review fiexes (comment typos)

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2025-03-11 17:49:01 +00:00
RiotRobot f552370c26 Merge branch 'master' into develop 2025-03-11 14:34:48 +00:00
RiotRobot 5f3f08071f v37.1.0 2025-03-11 14:34:18 +00:00
David Baker db7e3e3cf3 Add disableKeyStorage() to crypto API (#4742)
* Add disableKeyStorage() to crypto API

As an all-in-one method for deleting all server side key storage on
the user's account (as the doc hopefully explains).

* Add test

* const

* Can't be disabled here
2025-03-06 11:16:28 +00:00
renovate[bot] 3e512711d7 Update matrix-org/sonarcloud-workflow-action action to v4 (#4741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-04 14:45:54 +00:00
renovate[bot] f636976f15 Update dependency typedoc-plugin-mdn-links to v5 (#4740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:11:26 +00:00
renovate[bot] 905df2f7ca Update dependency @stylistic/eslint-plugin to v4 (#4738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:11:22 +00:00
renovate[bot] c1c473e6c6 Update dependency typescript to v5.8.2 (#4736)
* Update dependency typescript to v5.8.2

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-04 14:05:14 +00:00
renovate[bot] 4835ca61ec Update guibranco/github-status-action-v2 digest to 5ef6e17 (#4733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:02:32 +00:00
renovate[bot] abdc8b2c2e Update typescript-eslint monorepo to v8.25.0 (#4737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 13:39:36 +00:00
renovate[bot] 89fe90d27c Update typedoc (#4735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 13:39:05 +00:00
renovate[bot] 86e0eb11cc Update dependency @types/node to v18.19.78 (#4734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 13:38:30 +00:00
RiotRobot df7f69b6a2 v37.1.0-rc.0 2025-03-04 12:44:29 +00:00
Michael Telatynski 71bffb6c1b Handle unexpected token refresh failures gracefully (#4731)
* Fix idempotency issue around token refresh

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Handle unexpected token refresh failures gracefully

e.g. connection errors, proxy errors differently from token invalidated errors

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-28 11:25:06 +00:00
Michael Telatynski 72b997d1f3 Fix idempotency issue around token refresh (#4730)
* Fix idempotency issue around token refresh

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-27 18:37:47 +00:00
Timo a3bbc49e02 MatrixRTC: MembershipManager test cases and deprecation of MatrixRTCSession.room (#4713)
* WIP doodles on MembershipManager test cases

* .

* initial membership manager test setup.

* Updates from discussion

* revert renaming comments

* remove unused import

* fix leave delayed event resend test.
It was missing a flush.

* comment out and remove unused variables

* es lint

* use jsdom instead of node test environment

* remove unused variables

* remove unused export

* temp

* review

* fixup tests

* more review

* remove wait for expect dependency

* flatten tests and add comments

* add more leave test cases

* use defer

* remove @jest/environment dependency

* Cleanup awaits and Make mock types more correct.
Make every mock return a Promise if the real implementation does return a pormise.

* remove flush promise dependency

* add linting to matrixrtc tests

* Add fix async lints and use matrix rtc logger for test environment.

* prettier

* change to MatrixRTCSession logger

* make accessing the full room deprecated

* remove deprecated usage of full room

* Clean up the deprecation

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2025-02-27 09:55:09 +00:00
Michael Telatynski d81929de4c Merge remote-tracking branch 'origin/develop' into develop 2025-02-25 13:16:49 +00:00
Michael Telatynski 27002a0d8f Fix release-npm output id
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-25 13:16:42 +00:00
RiotRobot bb2a0bec8c Merge branch 'master' into develop 2025-02-25 13:11:30 +00:00
RiotRobot 2df8876f60 v37.0.0 2025-02-25 13:11:01 +00:00
Michael Telatynski 502f0ec0eb Update release.yml permissions 2025-02-25 13:10:07 +00:00
Richard van der Hoff 1ddba3460f Add EventType.SecretRequest and EventType.SecretSend (#4728)
A couple of to-device message types which are in the spec but not in the
js-sdk.
2025-02-25 10:26:13 +00:00
Hugh Nimmo-Smith 524cb65c5d MatrixRTC: enforce Promise handling using eslint rules (#4725)
* MatrixRTC: enforce Promise handling using eslint rules

* Fix lints without behaviour change.

* fix not calling functions

---------

Co-authored-by: Timo <toger5@hotmail.de>
2025-02-24 20:15:07 +00:00
Richard van der Hoff ea770282ea Simplify code paths for building to-device MatrixEvents (#4729)
We don't need all the complicated stuff when we have a to-device event, so
let's simplify.
2025-02-24 17:51:42 +00:00
Michael Telatynski bcaa7f63c7 Update triage-stale.yml 2025-02-24 10:45:22 +00:00
Richard van der Hoff 8ab9025282 Update matrix-rust-sdk-crypto-wasm to v14.0.1. (#4710)
* Bump rust sdk to 14.0.0

* Remove duplicate type declarations

These now match the types in the underlying library, so can be removed.

* bump to 14.0.1

* Use new `OutgoingRequest` type from wasm library

* fix types

* update lockfile
2025-02-22 09:07:29 +00:00
Hubert Chathi e49a0a5013 Delete the dehydrated device when resetEncryption is called (#4727)
* delete the dehydrated device when resetEncryption is called

* add more tests to improve coverage
2025-02-21 21:03:47 +00:00
Michael Telatynski 5b939287cc Attest npm package provenance (#4724)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-19 12:31:09 +00:00
Ajay Bura 2d381ade22 Report backup key import progress on start and improve types (#4711)
* report key import progress on start and improve types

* fix lint

* add documentation for exported types

* link `ImportRoomKeyProgressData` type in `ImportRoomKeyStage`

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* link `ImportRoomKeyFetchProgress` in fetch stage doc

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* link `ImportRoomKeyLoadProgress` in load_keys stage

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* link `ImportRoomKeyProgressData` in `ImportRoomKeyFetchProgress` type doc

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* link `ImportRoomKeyProgressData` in `ImportRoomKeyLoadProgress` type doc

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* link `ImportRoomKeyStage.Fetch`

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* convert `ImportRoomKeyStage.LoadKeys` to link in `ImportRoomKeyLoadProgress` stage doc

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* remove whitespace

* improve `ImportRoomKeyStage.Fetch` stage doc

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* improve `ImportRoomKeyStage.LoadKeys ` stage doc

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-02-19 09:03:30 +00:00
renovate[bot] 266475c37d Update dependency @types/node to v18.19.76 (#4718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 14:04:16 +00:00
renovate[bot] 597db17e59 Update typescript-eslint monorepo to v8.24.0 (#4722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 13:56:23 +00:00
renovate[bot] 9ea5b512e6 Update typedoc (#4719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 13:51:18 +00:00
renovate[bot] 2ce57fd942 Update all non-major dependencies (#4720)
* Update all non-major dependencies

* Remove stale bs58 types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 13:46:05 +00:00
renovate[bot] 102fa91efe Update guibranco/github-status-action-v2 digest to 7ca807c (#4716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 13:44:19 +00:00
renovate[bot] 266e9257aa Update babel monorepo (#4717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 13:42:51 +00:00
renovate[bot] f7015b35a0 Update dependency @stylistic/eslint-plugin to v3.1.0 (#4721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 13:36:57 +00:00
RiotRobot 26caa90805 v37.0.0-rc.0 2025-02-18 12:59:47 +00:00
Adam Spiers 1892dc13e0 Mention caveat in README about IndexedDB outside browser (#4712)
If using the SDK outside the browser with end-to-end encryption,
IndexedDB usage needs to be disabled otherwise it will cause the
Rust code to panic.

See also:

- https://github.com/matrix-org/matrix-js-sdk/issues/4570
- https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/issues/168
- https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/issues/195#issuecomment-2661510343

Signed-off-by: Adam Spiers <github@adamspiers.org>
2025-02-18 10:25:06 +00:00
Timo 0efeac9b3e typos in client.ts (#4715)
Pulled out from https://github.com/matrix-org/matrix-js-sdk/pull/4713
2025-02-17 14:43:08 +00:00
Michael Telatynski f11e1910f5 Improve types around User Interactive Auth (#4709)
* Remove confused type UIAResponse

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update uia.ts

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-17 09:53:22 +00:00
Ajay Bura a1a0463229 Enable key upload to backups where we have the decryption key (#4677)
* disable key backup when both trust via signatures and private key fail

* test for enabling backup with decryption key

* enable backup with decryption key in legacy crypto

* fix formmating

* fix typo

* add local variable for backup trust in legacy crypto

* Update spec/integ/crypto/megolm-backup.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/integ/crypto/megolm-backup.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/integ/crypto/megolm-backup.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/backup.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* fix white space formatting

* remove redundant test

* fix trust check while receiving backup secret

* mock room key version request before storing backup key

* fix decryption key gossip test for untrusted backup info

* rename version to latestBackupVersion to match the doc comments

* Update src/rust-crypto/backup.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* remove test to stop key gossip when signature mismatch

* remove misleading checkKeyBackupAndEnable doc return comment

* Update src/rust-crypto/backup.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* use requestKeyBackupVersion to get latest version instead of checkKeyBackupAndEnable

* remove comment

* test for backup key gossip when no backup found

* test for backup key gossip when backup request error

* fix lint error

* fix test message typo

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* refactor repeated test logic into a single reusable function

* improve exceptBackup param and docs

* fix: expect private key inside test

* fix linting

* add return type for backup key retrieve function

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* improve doc for retrieveBackupPrivateKeyWithDelay

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* improve expectBackup param description

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* fix status code and formatting

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-02-14 14:32:33 +00:00
R Midhun Suresh 30aa66e680 Improve CONTRIBUTING.md and add code_style.md (#4708)
* Fix typo in README

* Add proper contributing guide

This is based on the same in element-web repo but with the following
changes:
1. Uses sign-off instead of CLA
2. Removes react, app specific instructions eg: tests do not mention
   playwright.

* Add code_style.md

Copied from element-web repo but react/css specific items have been
removed.

* Fix lint
2025-02-12 17:45:45 +00:00
RiotRobot b584f818f5 Merge branch 'master' into develop 2025-02-11 14:23:41 +00:00
RiotRobot 4d7616bd68 v36.2.0 2025-02-11 14:23:11 +00:00
Richard van der Hoff 554804cd10 Clean up typescript types related to rust crypto (#4706)
* Simplify bootstrapSecretStorage logic

might as well just export the keys immediately, rather than having multiple
tests.

* Clean up typescript types related to rust crypto

A forthcoming release of matrix-rust-sdk-crypto-wasm tightens up a number of
typescript types. In preparation, we need to get our house in order too.
2025-02-11 12:22:27 +00:00
Richard van der Hoff 33648a711c Remove deprecated PrefixedLogger interface (#4705)
* Add some tests for `logger`

* Remove deprecated `PrefixedLogger` interface

`PrefixedLogger` has been deprecated for some time, so let's remove it now,
while we have a major version bump.

We can tidy up some of the other logic while we're here.

Unfortunately lots of the code still uses `logger.log` which isn't exposed by
the `Logger` interface, so we need to keep exposing that where it was before.
2025-02-11 12:19:46 +00:00
Richard van der Hoff c537a361fb MatrixClient.setAccountData: await remote echo. (#4695)
* Rewrite `deleteAccountData` test

use fetch-mock rather than whatever this was

* `MatrixClient.setAccountData`: await remote echo

Wait for the echo to come back from the server before we assume the account
data has been successfully set

* Update integration tests

Fix up the integ tests which call `setAccountData` and now need a sync
response.

* Address review comment
2025-02-10 16:35:08 +00:00
Michael Telatynski 30d9b0518f Fix permissions 2025-02-07 13:26:18 +00:00
Michael Telatynski 900c782216 Close stale PRs after 180 days (#4698)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-07 12:48:43 +00:00
Florian D 810f7142e6 Remove legacy crypto (#4653)
* Remove deprecated calls in `webrtc/call.ts`

* Throw error when legacy call was used

* Remove `MatrixClient.initLegacyCrypto` (#4620)

* Remove `MatrixClient.initLegacyCrypto`

* Remove `MatrixClient.initLegacyCrypto` in README.md

* Remove tests using `MatrixClient.initLegacyCrypto`

* Remove legacy crypto support in `sync` api (#4622)

* Remove deprecated `DeviceInfo` in `webrtc/call.ts` (#4654)

* chore(legacy call): Remove `DeviceInfo` usage

* refactor(legacy call): throw `GroupCallUnknownDeviceError` at the end of `initOpponentCrypto`

* Remove deprecated methods and attributes of `MatrixClient` (#4659)

* feat(legacy crypto)!: remove deprecated methods of `MatrixClient`

* test(legacy crypto): update existing tests to not use legacy crypto

- `Embedded.spec.ts`: casting since `encryptAndSendToDevices` is removed from `MatrixClient`.
- `room.spec.ts`: remove deprecated usage of `MatrixClient.crypto`
- `matrix-client.spec.ts` & `matrix-client-methods.spec.ts`: remove calls of deprecated methods of `MatrixClient`

* test(legacy crypto): remove test files using `MatrixClient` deprecated methods

* test(legacy crypto): update existing integ tests to run successfully

* feat(legacy crypto!): remove `ICreateClientOpts.deviceToImport`.

`ICreateClientOpts.deviceToImport` was used in the legacy cryto. The rust crypto doesn't support to import devices in this way.

* feat(legacy crypto!): remove `{get,set}GlobalErrorOnUnknownDevices`

`globalErrorOnUnknownDevices` is not used in the rust-crypto. The API is marked as unstable, we can remove it.

* Remove usage of legacy crypto in `event.ts` (#4666)

* feat(legacy crypto!): remove legacy crypto usage in `event.ts`

* test(legacy crypto): update event.spec.ts to not use legacy crypto types

* Remove legacy crypto export in `matrix.ts` (#4667)

* feat(legacy crypto!): remove legacy crypto export in `matrix.ts`

* test(legacy crypto): update `megolm-backup.spec.ts` to import directly `CryptoApi`

* Remove usage of legacy crypto in integ tests (#4669)

* Clean up legacy stores (#4663)

* feat(legacy crypto!): keep legacy methods used in lib olm migration

The rust cryto needs these legacy stores in order to do the migration from the legacy crypto to the rust crypto. We keep the following methods of the stores:
- Used in `libolm_migration.ts`.
- Needed in the legacy store tests.
- Needed in the rust crypto test migration.

* feat(legacy crypto): extract legacy crypto types in legacy stores

In order to be able to delete the legacy crypto, these stores shouldn't rely on the legacy crypto. We need to extract the used types.

* feat(crypto store): remove `CryptoStore` functions used only by tests

* test(crypto store): use legacy `MemoryStore` type

* Remove deprecated methods of `CryptoBackend` (#4671)

* feat(CryptoBackend)!: remove deprecated methods

* feat(rust-crypto)!: remove deprecated methods of `CryptoBackend`

* test(rust-crypto): remove tests of deprecated methods of `CryptoBackend`

* Remove usage of legacy crypto in `embedded.ts` (#4668)

The interface of `encryptAndSendToDevices` changes because `DeviceInfo` is from the legacy crypto. In fact `encryptAndSendToDevices` only need pairs of userId and deviceId.

* Remove legacy crypto files (#4672)

* fix(legacy store): fix legacy store typing

In https://github.com/matrix-org/matrix-js-sdk/pull/4663, the storeXXX methods were removed of the CryptoStore interface but they are used internally by IndexedDBCryptoStore.

* feat(legacy crypto)!: remove content of `crypto/*` except legacy stores

* test(legacy crypto): remove `spec/unit/crypto/*` except legacy store tests

* refactor: remove unused types

* doc: fix broken link

* doc: remove link tag when typedoc is unable to find the CryptoApi

* Clean up integ test after legacy crypto removal (#4682)

* test(crypto): remove `newBackendOnly` test closure

* test(crypto): fix duplicate test name

* test(crypto): remove `oldBackendOnly` test closure

* test(crypto): remove `rust-sdk` comparison

* test(crypto): remove iteration on `CRYPTO_BACKEND`

* test(crypto): remove old legacy comments and tests

* test(crypto): fix documentations and removed unused expect

* Restore broken link to `CryptoApi` (#4692)

* chore: fix linting and formatting due to merge

* Remove unused crypto type and missing doc (#4696)

* chore(crypto): remove unused types

* doc(crypto): add missing link

* test(call): add test when crypto is enabled
2025-02-07 12:31:40 +00:00
ElementRobot 6d7699cb4a feat(event): deprecate parameter and functions using legacy crypto (#4697) (#4700)
(cherry picked from commit 6b93e11e2c)

Co-authored-by: Florian D <florianduros@element.io>
2025-02-06 14:53:08 +00:00
Florian D 6b93e11e2c feat(event): deprecate parameter and functions using legacy crypto (#4697) 2025-02-06 13:24:52 +00:00
Hugh Nimmo-Smith ff1db2b538 Bump eslint-plugin-matrix-org to enable @typescript-eslint/consistent-type-imports rule (#4680)
* Bump eslint-plugin-matrix-org to enable @typescript-eslint/consistent-type-imports rule

* Re-lint after merge
2025-02-05 12:15:20 +00:00
Florian Duros 6e72b3554e Fix resetEncryption to remove secrets in 4S (#4683)
* fix(crypto): `resetEncryption` remove secrets in 4S

Remove the cross signing keys and the backup decryption key of the 4S when calling `resetEncryption`

* test(crypto): expect secrets to be deleted in 4S when `resetEncryption` is called

* test(secret storage): add test case when the secret is set at null

* fix(crypto): remove default key in 4S

* test(crypto): default key should be removed from 4S
2025-02-05 10:45:56 +00:00
renovate[bot] 7d687533e7 Update dependency typedoc-plugin-mdn-links to v4.0.10 (#4688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 15:15:58 +00:00
renovate[bot] 432c4eceb9 Update typescript-eslint monorepo to v8.22.0 (#4690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 15:01:47 +00:00
renovate[bot] 466ad3ef02 Update dependency @stylistic/eslint-plugin to v3 (#4691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 14:56:06 +00:00
renovate[bot] 5dd213b790 Update babel monorepo to v7.26.7 (#4686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 14:47:23 +00:00
renovate[bot] bdc3513e7a Update guibranco/github-status-action-v2 digest to 119b332 (#4684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 14:35:25 +00:00
renovate[bot] c520f70e92 Update dependency eslint-config-prettier to v10 (#4645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 14:18:01 +00:00
renovate[bot] 25c8c6aaf4 Update dependency @types/node to v18.19.74 (#4687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 14:17:44 +00:00
renovate[bot] 53610fa5a7 Update all non-major dependencies (#4685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 14:17:36 +00:00
Michael Telatynski ea34cce00a Apply lint rule @typescript-eslint/no-empty-object-type (#4679) 2025-02-04 13:41:32 +00:00
RiotRobot 6b0d2a4ad3 v36.2.0-rc.0 2025-02-04 12:29:59 +00:00
Michael Telatynski 72519a0eb4 Make sonarcloud.yml more reusable (#4681)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-03 14:25:56 +00:00
Will Hunt 624b8a242e Fix topic types (#4678) 2025-02-03 09:03:58 +00:00
Michael Telatynski 5be104a35c Fix types around Terms (#4674) 2025-02-03 08:48:00 +00:00
Will Hunt c93128ed39 Handle empty m.room.topic (#4673)
* Define topic as optional.

* Change isProvided so that types work better.

* allow makeTopicContent and parseTopicContent to handle optional values for plain text

* linting

* Remove usage of optional

* Topic key may only contain legacy key.

* Add tests for other branches.
2025-02-03 08:13:44 +00:00
Hubert Chathi cc238c24ab Provide more options for starting dehydration (#4664)
* provide more options for starting dehydration

* improve doc comments and tests
2025-01-30 18:22:23 +00:00
RiotRobot 8175683d4b Merge branch 'master' into develop 2025-01-28 13:21:56 +00:00
RiotRobot 4e54b7aab4 v36.1.0 2025-01-28 13:21:25 +00:00
Valere a2fd06bdf9 Add API to withdraw verification requirement CryptoAPI.withdrawVerificationRequirement (#4646)
* API to withdraw verification `CryptoAPi.withdrawVerificationRequirement`

* review: use set up function instead of beforeEach
2025-01-28 10:41:37 +00:00
Valere 7d8cbd6ef0 Device Dehydration | js-sdk: store/load dehydration key (#4599)
* feat(dehydrated): Use the dehydrated key cache API

* feat(dehydrated): Add signalling to device dehydration manager

* feat(dehydrated): fix unneeded call getCachedKey

* Upgrade to `matrix-sdk-crypto-wasm` v13.0.0

* review: quick fix and doc

* apply changes from review

* apply changes from review

* fix comment

* add some tests and emit an event on rehydration failure

* factor out event counter into a test util, since it may be useful elsewhere

* adjust test to cover a few more lines

* fix documentation

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* fix missing bracket

* add test for getting the dehydration key from SSSS

---------

Co-authored-by: Hubert Chathi <hubertc@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-01-27 22:05:23 +00:00
David Baker 44158bc843 Add unspecced backup disable flag (#4661)
* Add unspecced backup disable flag

* Add comment
2025-01-27 19:51:28 +00:00
ElementRobot e4590cac94 Revert "Distinguish room state and timeline events in embedded clients (#4574)" (#4656) (#4657)
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2025-01-27 10:36:17 +00:00
Hugh Nimmo-Smith fd9a44e701 Revert "Distinguish room state and timeline events in embedded clients (#4574)" (#4656)
This reverts commit bdd4d82cb3.
2025-01-27 10:04:03 +00:00
Hubert Chathi df492800b4 Fix test that will break with matrix-sdk-crypto-wasm 13.0.0 (#4635)
* fix test that will break with matrix-sdk-crypto-wasm 13.0.0

* this test requires using a real timer
2025-01-24 16:47:45 +00:00
Michael Telatynski 161da05972 Tidy up some main exports (#4649)
Fix knip config

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-23 15:41:29 +00:00
Florian Duros ed397d99ed CryptoApi.resetEncryption should always create a new key backup (#4648)
* fix(crypto api): `resetEncryption` always calls `resetKeyBackup`

* test(crypto api): update `resetEncryption` tests

* chore(crypto api): add logging in `resetEncryption`
2025-01-23 15:35:37 +00:00
Michael Telatynski c0e30ceca0 Switch OIDC primarily to new /auth_metadata API (#4626) 2025-01-22 13:48:27 +00:00
Florian Duros 61375ef38a Add CryptoApi.resetEncryption (#4614)
* feat(crypto api): Add `CryptoApi#resetEncryption`

* docs(crypto api): Review changes

* test(crypto api): Cleaner way to handle key backup removal
2025-01-22 10:53:50 +00:00
renovate[bot] e5fda72884 Update dependency @stylistic/eslint-plugin to v2.13.0 (#4642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 16:05:16 +00:00
renovate[bot] acfc619f31 Update dependency @types/node to v18.19.71 (#4637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 15:47:02 +00:00
renovate[bot] 7a0af07bf7 Update all non-major dependencies (#4640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 15:37:01 +00:00
renovate[bot] ad1afbc45b Update guibranco/github-status-action-v2 digest to ecd54a0 (#4636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:56:15 +00:00
renovate[bot] b4ce4ce184 Update dependency @babel/eslint-parser to v7.26.5 (#4641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:55:12 +00:00
renovate[bot] ddbc66b905 Update typescript-eslint monorepo to v8.20.0 (#4643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:53:48 +00:00
renovate[bot] ad1c67e33e Update dependency typescript to v5.7.3 (#4639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:53:09 +00:00
renovate[bot] d473e16df7 Update dependency typedoc-plugin-mdn-links to v4.0.8 (#4638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 14:53:05 +00:00
RiotRobot 806538828e v36.1.0-rc.0 2025-01-21 14:40:56 +00:00
David Baker 424c258a07 Merge pull request #4621 from matrix-org/dbkr/secure_random_string
Change randomString et al to be secure
2025-01-21 13:54:50 +00:00
David Baker ea67d3977e Fix tsdoc formatting & consistency
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-01-21 13:32:28 +00:00
David Baker 8bffa39eb9 Add link
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-01-21 13:29:44 +00:00
David Baker f13967aaec Use modulo arithmetic instead
also I think this was just wrong in that it was subtracting 1
unnercessarily because we already used < rather than <= below.
2025-01-21 09:50:55 +00:00
Richard van der Hoff b496601712 Add an extra consistency check in bootstrapCrossSigning (#4629)
* Add an extra consistency check in `bootstrapCrossSigning`

check that `importCrossSigningKeys` has actually worked

* Update src/rust-crypto/CrossSigningIdentity.ts

* declare type in @types, instead of in source
2025-01-20 21:21:05 +00:00
Richard van der Hoff ce60162827 Deprecate MatrixClient.login and replace with loginRequest (#4632)
`MatrixClient.login` has some very unintuitive behaviour where it
stashes the access token, but not the device id, refresh token, etc etc, which
led people to imagine that they had a functional `MatrixClient` when they
didn't. In practice, you have to create a *new* `MatrixClient` given the `LoginResponse`.

As the first step for sorting this out, this deprecates the broken method and
replaces it with one that has sensible behaviour.
2025-01-20 21:18:51 +00:00
David Baker 1f1d6f0bc8 Document exports 2025-01-20 18:27:40 +00:00
David Baker 35fe7bc60a Use a random impl with rejection sampling 2025-01-20 18:13:26 +00:00
Richard van der Hoff b45d51a131 Fix documentation on CryptoEvent (#4628)
* Fix documentation on `CryptoEvent`

`CryptoApi` itself does not emit events (or at least, its public type
information does not allow you to listen for events emitted by CryptoApi).

* fix link
2025-01-20 17:04:17 +00:00
Florian Duros a6fd28b03d feat(sliding sync): Use SyncCryptoCallback api instead of legacy crypto (#4624) 2025-01-17 11:45:13 +00:00
Florian Duros 78e7e2af31 Use rust crypto instead of legacy crypto in sync tests (#4623) 2025-01-16 17:27:33 +00:00
David Baker 86494c3a96 Change randomString et al to be secure
...and renames them, removing the special lowercase and uppercase
versions and exporting the underlying function instead.

Any apps that use these will either need to take the speed hit from
secure random functions and use the new ones, or write their own
insecure versions.

The lowercase and uppercasde verisons were used exactly once each
in element-web and never in js-sdk itself. The underlying function
is very simple and exporting just this gives more flexibility with
fewer exports.
2025-01-16 14:48:36 +00:00
Robin bdd4d82cb3 Distinguish room state and timeline events in embedded clients (#4574)
* Distinguish room state and timeline events in embedded clients

This change enables room widget clients to take advantage of the more reliable method of communicating room state over the widget API provided by a recent update to MSC2762.

* Add missing awaits

* Upgrade matrix-widget-api
2025-01-15 18:14:05 +00:00
Florian Duros 5babcaf4b3 feat(secret storage): keyId in SecretStorage.setDefaultKeyId can be set at null in order to delete an exising recovery key (#4615) 2025-01-15 12:29:02 +00:00
m004 ffbb4716c4 Add authenticated media to getAvatarURL in room and room-member models (#4616) 2025-01-15 09:49:18 +00:00
RiotRobot 07f97d724f Merge branch 'master' into develop 2025-01-14 14:09:25 +00:00
RiotRobot 72ee5504d5 v36.0.0 2025-01-14 14:08:54 +00:00
Timo 9134471dc7 MatrixRTC: refactor MatrixRTCSession media encryption key logic into EncryptionManager (#4612)
* move Encryption logic from MatrixRTCSession into EncryptionManager

* review

* review 2
2025-01-14 10:20:51 +00:00
Timo f22d5e9d47 MatrixRTC: additional TS docs for IMembershipManager interface (#4613)
* docstrings for IMembershipManager interface

* more details and cleanup

* timeout docs
2025-01-13 18:29:50 +00:00
Timo ffb228bf5a MatrixRTC: refactor MatrixRTCSession MemberManager API (#4610)
* update join and leave internal api.

* rename onMembershipUpdate and triggerCallMembershipEventUpdate to onMembershipsUpdate
This makes it more clear that we do not talk about our own membership but all memberships in the session

* cleanup MembershipManager
 - add comments and interface how to test this class.
 - sort methods by public/private
 - make triggerCallMembershipEventUpdate private

* docstrings for getFocusInUse and getActiveFocus

* simplify tests and make them only use MembershipManagerInterface methods.
This allows to exchange the membershipManager with a different implementation.

* convert interface to abstract class.

* review (implement interface, make interface internal, dont change public api.)

* Make the interface an actual interface.
The actual constructor of the class now contains the `Pick` to define what it needs from the client.

* move update condition into MembershipManager

* renaming public api

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2025-01-13 13:20:54 +00:00
David Baker bed4e9579e Mark MSC3981 as stable in Matrix 1.10 (#4023)
So we will send the 'recurse' parameter unprefixed for servers that
support 1.10 (when it's released).
2025-01-13 09:49:15 +00:00
Timo 7697338c7e MatrixRTC: move MatrixRTCSession logic into LocalMembershipManager (#4608)
* split joinConfig
 - myMembership related properties get moved into its own interface

* Add MyMembershipManager

* Remove methods and functions that are from MatrixRTCSession (they now live in MyMembershipManager)

* Refactor MatrixRTCSession to use myMembershipManager

* fix tests

* review

* get rid of more memberhsip manager usage in tests

* review - fix tests using private membershipManager props

* fix circular import
2025-01-10 10:46:28 +00:00
Michael Telatynski 1da26b5cd1 Fix issue with sentinels being incorrect on m.room.member events (#4609)
* Fix issue with sentinels being incorrect on m.room.member events

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Simplify change

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-10 10:16:45 +00:00
renovate[bot] 5b85ae491e Update typescript-eslint monorepo to v8.19.0 (#4607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 14:02:30 +00:00
renovate[bot] 2024b070b0 Update typedoc (#4604)
* Update typedoc

* Make typedoc happier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-07 13:39:07 +00:00
renovate[bot] eef964f07d Update dependency @stylistic/eslint-plugin to v2.12.1 (#4606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 13:16:07 +00:00
renovate[bot] 1e9c119159 Update dependency @types/node to v18.19.69 (#4603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 12:55:34 +00:00
renovate[bot] fd894309f2 Update guibranco/github-status-action-v2 digest to 56cd38c (#4602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 12:54:50 +00:00
renovate[bot] 75486b72a6 Update all non-major dependencies (#4605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 12:53:53 +00:00
RiotRobot 38816589f5 v36.0.0-rc.0 2025-01-07 12:43:09 +00:00
Timo 6f743bfa1f MatrixRTC: Implement expiry logic for CallMembership and additional test coverage (#4587)
* remove all legacy call related code and adjust tests.
We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests.

* dont adjust tests but remove legacy tests

* Remove deprecated CallMembership.getLocalExpiry()

* Remove references to legacy in test case names

* Clean up SessionMembershipData tsdoc

* Remove CallMembership.expires

* Use correct expire duration.

* make expiration methods not return optional values and update docstring

* add docs to `SessionMembershipData`

* Add new tests for session type member events that before only existed for legacy member events.

This reverts commit 795a3cffb61d672941c49e8139eb1d7b15c87d73.

* remove code we do not need yet.

* Cleanup

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2025-01-07 10:14:09 +00:00
Timo ffd3c9575e Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership (#4583)
* remove all legacy call related code and adjust tests.
We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests.

* dont adjust tests but remove legacy tests

* Remove deprecated CallMembership.getLocalExpiry()

* Remove references to legacy in test case names

* Clean up SessionMembershipData tsdoc

* Remove CallMembership.expires

* Use correct expire duration.

* make expiration methods not return optional values and update docstring

* add docs to `SessionMembershipData`

* Use `MSC4143` (instaed of `non-legacy`) wording in comment

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>

* Incorporate feedback from review

* Fix test name

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2025-01-06 17:23:16 +00:00
David Baker 7678923e04 Don't retry on 4xx responses (#4601)
* Don't retry on 4xx responses

I'm not sure why this was limited to a small set of 4xx responses.
Nominally, no 4xx request should be retried (in fact the comment
below says this, but then the code didn't quite match it).

This was causing key backup requests to be retried even when the
server responded 404 because the backup in question had been deleted,
meaning the client would retry uselessly and it would take longer for
the client to prompt the user for action.

* Exclude 429s
2025-01-06 13:25:29 +00:00
Johannes Marbach 6f7c74f9ea Add syntax & type check for Node.js example on CI (#4410)
* Add syntax & type check for Node.js example on CI

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Fix quotes

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
Co-authored-by: Florian Duros <florianduros@element.io>
2025-01-03 18:49:57 +00:00
Michael Telatynski 3fcc56601b Use mapped types for account data content (#4590)
* Use mapped types around account data events

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Harden types for reading account data too

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Correct empty object type

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update src/secret-storage.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-12-19 22:53:58 +00:00
Andy Balaam bcf3d56bd5 Upgrade matrix-sdk-crypto-wasm to 12.1.0 (#4596)
... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424
2024-12-19 14:33:25 +00:00
RiotRobot 647a2a1a19 Merge branch 'master' into develop 2024-12-18 14:11:02 +00:00
RiotRobot 1bf8533c03 v35.1.0 2024-12-18 14:10:31 +00:00
David Baker fb2f2dd6d4 Merge pull request #4593 from matrix-org/rav/migration-for-previouslyverified
Upgrade matrix-sdk-crypto-wasm to 1.11.0
2024-12-18 14:01:18 +00:00
Richard van der Hoff d33350ff26 Upgrade matrix-sdk-crypto-wasm to 1.11.0
... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424
2024-12-18 13:29:05 +00:00
RiotRobot 349a86c119 Merge branch 'master' into develop 2024-12-17 13:22:36 +00:00
Hugh Nimmo-Smith e4182eb752 Update matrix-sdk-crypto-wasm to 12.0.0 (#4589)
It appears to "just work"... but I might be missing something
2024-12-17 11:24:46 +00:00
David Baker 3219aefc92 Avoid key prompts when resetting crypto (#4586)
* Avoid key prompts when resetting crypto

Attempting to get the backup key out of secret storage can cause
the user to be prompted for their key, which is not helpful if this
is being done as part of a reset. This check was redundant anyway
and we can just overwrite the key with the same value.

Also fix docs and remove check for active backup.

* Fix doc
2024-12-17 09:22:31 +00:00
Richard van der Hoff aba4e690af Improve documentation on various secret-storage related methods (#4585)
* Improve documentation on various secret-storage related methods

* fix link

* Apply suggestions from code review
2024-12-16 13:00:18 +00:00
Liam Diprose 693bb22ba1 Handle when aud OIDC claim is an Array (#4584)
* Handle when `aud` OIDC claim is an Array

The `aud` claim of OIDC id_tokens [can be an array](https://github.com/authts/oidc-client-ts/blob/ce6d694639c58e6a1c80904efdac5eda82b82042/src/Claims.ts#L92) but the existing logic
incorrectly assumes `aud` is always a string.

This PR adds the necessary check.

* Clarify `aud` OIDC claim check

* Fix for prettier

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-16 11:38:34 +00:00
renovate[bot] 315e81b7de Update typescript-eslint monorepo to v8.17.0 (#4581)
* Update typescript-eslint monorepo to v8.17.0

* Fix lint errors

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-12 17:45:39 +00:00
David Baker a0502c5ee5 Save the key backup key to 4S during bootstrapCrossSigning (#4542)
* Save the key backup key to secret storage

When setting up secret storage, if we have a key backup key in cache
(like we do for the cross signing secrets).

* Add test

* Get the key directly from the olmMachine

saves converting it needlessly into a buffer to turn it back into
a base64 string

* Overwrite backup keyin storage if different

* Fix test

* Add integ test

* Test failure case for sonar

* Unused import

* Missed return

* Also check active backup version
2024-12-12 15:03:19 +00:00
Timo d1de32ea27 Only re-prepare MatrixrRTC delayed disconnection event on 404 (#4575)
* Set retry counts of event updating to 1000 (from 1)

 With it being set to one the following issue could occur:

```
// 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,
                ),
            1000,
        );
    } catch (e) {
        logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
        this.disconnectDelayId = undefined;
        await prepareDelayedDisconnection();
    }
}
```
This code looks like the `catch(e)` could never be triggered with 429 (rate limit) because they would be caught by `await resendIfRateLimited`. EXCEPT that this is only happening once: `resendIfRateLimited<T>(func: () => Promise<T>, numRetriesAllowed: number = 1)`. So as soon as the server sends two rate limits in a row we get the following:
 - we get into the `catch(e)`  because of the rate limit
 - we forget about `this.disconnectDelayId = undefined`
 - we start a new delayed event `await prepareDelayedDisconnection();`
 - we do not anymore update the old delayed event which is still running!
 - the running delay event will make us disconnect from the call (call member becomes `{}`)
 - we get into our outher error catching mechanism that resends the new state event
 - this cancels the newly created delay leave event (`await prepareDelayedDisconnection();`)
 - and create another delay leave event.
 - but if we are still reate limited (chances are really high due to the reconnect), this loop will REPEAT

* also check for M_NOT_FOUND

* Leave retry at current level

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2024-12-12 14:58:50 +00:00
renovate[bot] 3ae25427a8 Update dependency @types/node to v18.19.67 (#4578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 09:53:08 +00:00
renovate[bot] 8155b0acfc Update mheap/github-action-required-labels digest to 388fd6a (#4577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 22:11:53 +00:00
renovate[bot] 413c156624 Update guibranco/github-status-action-v2 digest to d469d49 (#4576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 18:12:27 +00:00
renovate[bot] e78a3cec9f Update all non-major dependencies (#4579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 16:50:03 +00:00
renovate[bot] 5998de365d Update dependency @babel/cli to v7.26.4 (#4580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 16:31:44 +00:00
440 changed files with 36289 additions and 49746 deletions
+47 -15
View File
@@ -1,6 +1,6 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc", "n"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
},
@@ -83,17 +83,6 @@ module.exports = {
],
},
],
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"jest/no-disabled-tests": "off",
// Also treat "oldBackendOnly" as a test function.
// Used in some crypto tests.
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
},
],
},
overrides: [
{
@@ -112,8 +101,13 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// We do this sometimes to brand interfaces
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-empty-object-type": [
"error",
{
// We do this sometimes to brand interfaces
allowInterfaces: "with-single-extends",
},
],
"quotes": "off",
// We use a `logger` intermediary module
@@ -143,10 +137,48 @@ module.exports = {
},
{
files: ["spec/**/*.ts"],
extends: ["plugin:@vitest/legacy-recommended"],
rules: {
// We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-empty-object-type": "off",
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"@vitest/no-disabled-tests": "off",
// Used in some crypto tests.
"@vitest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
},
],
"@vitest/expect-expect": [
"error",
{
assertFunctionNames: [
"expect",
"expectDevices",
"assert.isTrue",
"assert.isFalse",
"passwordTest",
"compareHeaders",
"doTest",
],
},
],
},
},
{
// Enable stricter promise rules for the MatrixRTC codebase
files: ["src/matrixrtc/**/*.ts", "spec/unit/matrixrtc/*.ts"],
rules: {
// Encourage proper usage of Promises:
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/await-thenable": "error",
},
},
],
+1 -1
View File
@@ -1,7 +1,7 @@
* @matrix-org/element-web-reviewers
/.github/workflows/** @matrix-org/element-web-team
/package.json @matrix-org/element-web-team
/yarn.lock @matrix-org/element-web-team
/pnpm-lock.yaml @matrix-org/element-web-team
/scripts/** @matrix-org/element-web-team
/src/webrtc @matrix-org/element-call-reviewers
/src/matrixrtc @matrix-org/element-call-reviewers
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+3
View File
@@ -41,3 +41,6 @@
- name: "Z-Flaky-Test"
description: "A test is raising false alarms"
color: "ededed"
- name: "Z-Skip-Coverage"
description: "Skip SonarQube coverage for this PR"
color: "ededed"
+3 -1
View File
@@ -1,6 +1,8 @@
name: Backport
on:
pull_request_target:
# Privilege escalation necessary to enable backporting PRs from forks
# 🚨 We must not execute any checked out code here.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- closed
- labeled
+5 -3
View File
@@ -1,7 +1,9 @@
name: Deploy documentation PR preview
on:
workflow_run:
# Privilege escalation necessary to publish to Netlify
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Static Analysis"]
types:
- completed
@@ -15,7 +17,7 @@ jobs:
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -23,7 +25,7 @@ jobs:
path: docs
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@v3
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
with:
path: docs
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
@@ -1,7 +1,7 @@
# Triggers after the "Downstream artifacts" build has finished, to run the
# matrix-react-sdk playwright tests (with access to repo secrets)
# element-web playwright tests (with access to repo secrets)
name: matrix-react-sdk End to End Tests
name: Element Web End to End Tests
on:
merge_group:
types: [checks_requested]
@@ -21,11 +21,12 @@ concurrency:
jobs:
playwright:
name: Playwright
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
permissions:
actions: read
issues: read
pull-requests: read
contents: read
with:
matrix-js-sdk-sha: ${{ github.sha }}
# We only want to run the playwright tests on merge queue to prevent regressions
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+11 -6
View File
@@ -1,6 +1,9 @@
name: Pull Request
on:
pull_request_target:
# Privilege escalation necessary access members of the review teams
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, edited, labeled, unlabeled, synchronize]
merge_group:
types: [checks_requested]
@@ -15,7 +18,7 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-24.04
steps:
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -35,7 +38,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@@ -49,7 +52,7 @@ jobs:
pull-requests: write
steps:
- name: Check membership
if: github.event.pull_request.user.login != 'renovate[bot]'
if: github.event.pull_request.user.login != 'renovate[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
id: teams
with:
@@ -60,7 +63,7 @@ jobs:
- name: Add label
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.addLabels({
@@ -73,13 +76,15 @@ jobs:
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-24.04
permissions:
pull-requests: write
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.createComment({
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
REPO: ${{ inputs.repository }}
with:
@@ -16,18 +16,20 @@ jobs:
contents: write
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: staging
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: package.json
cache: "yarn"
cache: "pnpm"
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
id: draft-release
@@ -37,7 +39,7 @@ jobs:
disable-autolabeler: true
- name: Get actions scripts
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -48,7 +50,7 @@ jobs:
- name: Ingest upstream changes
if: inputs.include-changes
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
draft:
permissions:
contents: write
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
+13 -6
View File
@@ -12,20 +12,25 @@ on:
description: List of dependencies to reset.
type: string
required: false
dir:
description: The directory to release
type: string
default: "."
concurrency: ${{ github.workflow }}
permissions: {} # Uses ELEMENT_BOT_TOKEN
jobs:
merge:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
persist-credentials: true
- name: Get actions scripts
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -33,13 +38,14 @@ jobs:
sparse-checkout: |
scripts/release
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Set up git
run: |
@@ -53,6 +59,7 @@ jobs:
- name: Reset dependencies
if: inputs.dependencies
working-directory: ${{ inputs.dir }}
run: |
while IFS= read -r PACKAGE; do
[ -z "$PACKAGE" ] && continue
@@ -73,7 +80,7 @@ jobs:
fi
echo "Resetting $PACKAGE to develop branch..."
yarn add "github:matrix-org/$PACKAGE#develop"
pnpm add "github:matrix-org/$PACKAGE#develop"
git add -u
git commit -m "Reset $PACKAGE back to develop branch"
done <<< "$DEPENDENCIES"
+42 -23
View File
@@ -4,8 +4,6 @@ on:
secrets:
ELEMENT_BOT_TOKEN:
required: true
NPM_TOKEN:
required: false
GPG_PASSPHRASE:
required: false
GPG_PRIVATE_KEY:
@@ -28,12 +26,21 @@ on:
description: |
The path to the asset you want to upload, if any. You can use glob patterns here.
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
Relative to `dir`.
type: string
required: false
expected-asset-count:
description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number
required: false
dist-dir:
description: The directory to release
type: string
default: "."
version-dirs:
description: Directories in which to update package.json `version` field
type: string
required: false
outputs:
npm-id:
description: "The npm package@version string we published"
@@ -45,7 +52,7 @@ jobs:
permissions:
issues: read
pull-requests: read
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
release:
name: Release
@@ -58,7 +65,7 @@ jobs:
- name: Load GPG key
id: gpg
if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -66,22 +73,23 @@ jobs:
- name: Get draft release
id: draft-release
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
env:
GITHUB_TOKEN: ${{ github.token }}
with:
draft: true
latest: true
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: staging
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
persist-credentials: true
- name: Get actions scripts
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -92,6 +100,7 @@ jobs:
- name: Prepare variables
id: prepare
working-directory: ${{ inputs.dist-dir }}
run: |
echo "VERSION=$VERSION" >> $GITHUB_ENV
@@ -106,7 +115,7 @@ jobs:
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
- name: Check version number not in use
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { VERSION } = process.env;
@@ -125,15 +134,17 @@ jobs:
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
node-version-file: package.json
cache: "pnpm"
node-version-file: ${{ inputs.dist-dir }}/package.json
- name: Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Handle develop dependencies
working-directory: ${{ inputs.dist-dir }}
run: |
ret=0
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
@@ -142,15 +153,19 @@ jobs:
VERSION=${dep[1]}
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
yarn upgrade "$PACKAGE@$VERSION" --exact
pnpm add "$PACKAGE@$VERSION" --save-exact
git add -u
git commit -m "Keep $PACKAGE at $VERSION"
done
- name: Bump package.json version
- name: Bump package.json versions
run: |
yarn version --no-git-tag-version --new-version "${VERSION#v}"
git add package.json
for DIR in $DIRS; do
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
git add "$DIR"/package.json
done
env:
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
- name: Add to CHANGELOG.md
if: inputs.final
@@ -177,7 +192,8 @@ jobs:
- name: Build assets
if: steps.prepare.outputs.has-dist-script == '1'
run: DIST_VERSION="$VERSION" yarn dist
working-directory: ${{ inputs.dist-dir }}
run: DIST_VERSION="$VERSION" pnpm dist
- name: Upload release assets & signatures
if: inputs.asset-path
@@ -185,7 +201,7 @@ jobs:
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.draft-release.outputs.upload_url }}
asset-path: ${{ inputs.asset-path }}
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
- name: Create signed tag
if: inputs.gpg-fingerprint
@@ -218,7 +234,7 @@ jobs:
- name: Validate release has expected assets
if: inputs.expected-asset-count
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
@@ -246,7 +262,7 @@ jobs:
git push origin master
- name: Publish release
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
FINAL: ${{ inputs.final }}
@@ -278,9 +294,12 @@ jobs:
name: Publish to npm
needs: release
if: inputs.npm
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
with:
dir: ${{ inputs.dist-dir }}
permissions:
contents: read
id-token: write
post-release:
name: Post release steps
+27 -26
View File
@@ -1,52 +1,53 @@
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
inputs:
dir:
description: The directory to release
type: string
default: "."
outputs:
id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
permissions: {} # No permissions required
permissions: {}
jobs:
npm:
name: Publish to npm
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
outputs:
id: ${{ steps.npm-publish.outputs.id }}
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: staging
persist-credentials: false
- name: 🔧 Yarn cache
uses: actions/setup-node@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
node-version-file: ${{ inputs.dir }}/package.json
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c # v3.1.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
ignore-scripts: false
- name: Check npm package was published
if: steps.npm-publish.outputs.id == ''
run: exit 1
- name: 🎖️ Add `latest` dist-tag to final releases
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
run: npm dist-tag add "$release" latest
working-directory: ${{ inputs.dir }}
run: |
npm publish --provenance --access public --tag "$TAG"
release=$(jq -r '"\(.name)@\(.version)"' package.json)
echo "id=$release" >> $GITHUB_OUTPUT
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.id }}
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
+25 -17
View File
@@ -24,11 +24,12 @@ concurrency: ${{ github.workflow }}
permissions: {} # No permissions required
jobs:
release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
permissions:
contents: write
issues: write
pull-requests: read
id-token: write
secrets: inherit
with:
final: ${{ inputs.mode == 'final' }}
@@ -40,28 +41,32 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
repo:
- element-hq/element-web
include:
- repo: element-hq/element-web
path: apps/web
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
persist-credentials: true
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
DIR: ${{ matrix.path }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
git add "$DIR"/package.json pnpm-lock.yaml
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
@@ -72,22 +77,25 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
- name: 🔧 Yarn cache
uses: actions/setup-node@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: 📖 Generate docs
run: yarn gendoc
run: pnpm gendoc
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: _docs
@@ -105,4 +113,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
+15 -10
View File
@@ -12,7 +12,11 @@ on:
sharded:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
version-pkg-json-dir:
type: string
default: "."
description: "Relative path of the directory containing package.json with the `version` to use."
permissions: {}
jobs:
sonarqube:
@@ -27,7 +31,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@66088c44e212a906c32a047529a213d81809ec1c
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -36,14 +40,15 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
persist-credentials: false
- name: 📥 Download artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,14 +56,13 @@ jobs:
name: coverage
path: coverage
- name: 📥 Download sharded artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: inputs.sharded
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: coverage-*
path: coverage
merge-multiple: true
- name: Check coverage artifact
run: |
if [ ! -d coverage ]; then
@@ -70,24 +74,25 @@ jobs:
run: |
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
reports=$(find coverage -type f -name '*sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v3.3
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
skip_checkout: true
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
version_cmd: "cat package.json | jq -r .version"
skip_coverage_label: Z-Skip-Coverage
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
- uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+4 -2
View File
@@ -1,6 +1,8 @@
name: SonarQube
on:
workflow_run:
# Privilege escalation necessary to call upon SonarCloud
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Tests"]
types:
- completed
@@ -16,7 +18,7 @@ jobs:
actions: read
statuses: write
id-token: write # sonar
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+102 -41
View File
@@ -14,72 +14,121 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Typecheck
run: "yarn run lint:types"
run: "pnpm run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Run Linter
run: "yarn run lint:js"
run: "pnpm run lint:js"
node_example_lint:
name: "Node.js example"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "pnpm install"
- name: Build Types
run: "pnpm build:types"
- name: Install Example Deps
run: "npm install"
working-directory: "examples/node"
- name: Check Syntax
run: "node --check app.js"
working-directory: "examples/node"
- name: Typecheck
run: "npx tsc"
working-directory: "examples/node"
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-24.04
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Run Linter
run: "yarn lint:workflows"
run: "pnpm lint:workflows"
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
docs:
name: "JSDoc Checker"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docs
path: _docs
@@ -90,31 +139,36 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Run linter
run: "yarn run lint:knip"
run: "pnpm run lint:knip"
element-web:
name: Downstream tsc element-web
if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: element-hq/element-web
persist-credentials: false
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: "lts/*"
- name: Install Dependencies
@@ -124,15 +178,22 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
- name: Typecheck
run: "yarn run lint:types"
working-directory: apps/web
run: "pnpm run lint:types"
# Hook for branch protection to skip downstream typechecking outside of merge queues
downstream:
name: Downstream Typescript Syntax Check
# Workflow consolidation job
done:
needs:
- ts_lint
- js_lint
- node_example_lint
- workflow_lint
- docs
- analyse_dead_code
- element-web
name: Static Analysis
runs-on: ubuntu-24.04
if: always()
needs:
- element-web
steps:
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
+1 -1
View File
@@ -11,7 +11,7 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
with:
LABELS: |
element-hq/element-meta
+26 -24
View File
@@ -12,8 +12,8 @@ env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
permissions: {} # No permissions required
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
test:
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
@@ -22,17 +22,20 @@ jobs:
node: ["lts/*", 22]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node
id: setupNode
uses: actions/setup-node@v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: ${{ matrix.node }}
- name: Install dependencies
run: "yarn install"
run: "pnpm install"
- name: Get number of CPU cores
id: cpu-cores
@@ -40,24 +43,23 @@ jobs:
- name: Run tests
run: |
yarn test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--ci \
--max-workers ${{ steps.cpu-cores.outputs.count }} \
pnpm test \
--coverage=${ENABLE_COVERAGE} \
--maxWorkers ${NUM_WORKERS} \
./spec/${{ matrix.specs }}
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
# tell jest to use coloured output
FORCE_COLOR: true
SHARD: ${{ matrix.specs }}
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
env:
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
path: |
@@ -65,19 +67,19 @@ jobs:
!coverage/lcov-report
# Dummy completion job to simplify branch protections
jest-complete:
name: Jest tests
needs: jest
complete:
name: Tests
needs: test
if: always()
runs-on: ubuntu-24.04
steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
run: exit 1
element-web:
name: Downstream test element-web
if: github.event_name == 'merge_group'
uses: element-hq/element-web/.github/workflows/tests.yml@develop
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
permissions:
statuses: write
with:
@@ -87,8 +89,8 @@ jobs:
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
permissions: read-all
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
permissions: read-all # zizmor: ignore[excessive-permissions]
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
with:
use_js_sdk: "."
@@ -116,7 +118,7 @@ jobs:
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
automate-project-columns-next:
runs-on: ubuntu-24.04
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -6,6 +6,6 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
call-triage-labelled:
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+22
View File
@@ -0,0 +1,22 @@
name: Close stale PRs
on:
workflow_dispatch: {}
schedule:
- cron: "30 1 * * *"
permissions: {}
jobs:
close:
runs-on: ubuntu-24.04
permissions:
actions: write
issues: write
pull-requests: write
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with:
operations-per-run: 250
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 180
days-before-pr-close: 0
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
+1 -2
View File
@@ -13,8 +13,7 @@ out
/dist
/lib
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
# tarball created by `npm pack` / `yarn pack`
/matrix-js-sdk-*.tgz
.vscode
+1
View File
@@ -3,6 +3,7 @@
/.npmrc
/*.log
pnpm-lock.yaml
package-lock.json
.lock-wscript
build/Release
+520
View File
@@ -1,3 +1,523 @@
Changes in [41.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.3.0) (2026-04-07)
==================================================================================================
## 🐛 Bug Fixes
* Rotate the current room key when we see a member leave ([#5231](https://github.com/matrix-org/matrix-js-sdk/pull/5231)). Contributed by @kaylendog.
Changes in [41.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.2.0) (2026-03-24)
==================================================================================================
## ✨ Features
* Only share history if room history visibility is shared ([#5216](https://github.com/matrix-org/matrix-js-sdk/pull/5216)). Contributed by @kaylendog.
* History sharing: resume key-bundle import on restart ([#5214](https://github.com/matrix-org/matrix-js-sdk/pull/5214)). Contributed by @richvdh.
* Move `CryptoApi.shareRoomHistoryWithUser` to `CryptoBackend` ([#5218](https://github.com/matrix-org/matrix-js-sdk/pull/5218)). Contributed by @richvdh.
Changes in [41.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.1.0) (2026-03-10)
==================================================================================================
## ✨ Features
* Throw a specific error when the backup decryption key does not match the public backup ([#5202](https://github.com/matrix-org/matrix-js-sdk/pull/5202)). Contributed by @andybalaam.
* Update getUrlPreview to use /\_matrix/client/v1/media/preview\_url ([#5191](https://github.com/matrix-org/matrix-js-sdk/pull/5191)). Contributed by @Half-Shot.
Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
==================================================================================================
## 🚨 BREAKING CHANGES
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
## ✨ Features
* Download room keys from backup prior to buliding historic room key bundles ([#5171](https://github.com/matrix-org/matrix-js-sdk/pull/5171)). Contributed by @kaylendog.
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
* Add logging on MSC4108 DELETE request ([#5140](https://github.com/matrix-org/matrix-js-sdk/pull/5140)). Contributed by @reivilibre.
* Add `m.invite_permission_config` account data type ([#5183](https://github.com/matrix-org/matrix-js-sdk/pull/5183)). Contributed by @richvdh.
## 🐛 Bug Fixes
* fix(relations): prevent stale m.replace from overriding newer edits ([#5192](https://github.com/matrix-org/matrix-js-sdk/pull/5192)). Contributed by @basnijholt.
* Fix reactive display name disambiguation ([#5135](https://github.com/matrix-org/matrix-js-sdk/pull/5135)). Contributed by @aditya-cherukuru.
* Fix empty string to room compatibility trick to only apply to m.call ([#5172](https://github.com/matrix-org/matrix-js-sdk/pull/5172)). Contributed by @toger5.
Changes in [40.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.2.0) (2026-02-10)
==================================================================================================
## 🦖 Deprecations
* [MatrixRTC] Remove sending of deprecated `notify` event (we now use `m.rtc.notification`) ([#5167](https://github.com/matrix-org/matrix-js-sdk/pull/5167)). Contributed by @toger5.
## ✨ Features
* Use stable /auth\_metadata endpoint where homeserver supports v1.15 ([#5174](https://github.com/matrix-org/matrix-js-sdk/pull/5174)). Contributed by @hughns.
* Support additional\_creators in upgradeRoom (MSC4289) ([#5173](https://github.com/matrix-org/matrix-js-sdk/pull/5173)). Contributed by @andybalaam.
* [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId ([#5166](https://github.com/matrix-org/matrix-js-sdk/pull/5166)). Contributed by @toger5.
* [MatrixRTC] Do not send the `livekit_alias` in sticky events ([#5165](https://github.com/matrix-org/matrix-js-sdk/pull/5165)). Contributed by @toger5.
* Improve startup performance by using `promise.all` when processing rooms from sync ([#5095](https://github.com/matrix-org/matrix-js-sdk/pull/5095)). Contributed by @MidhunSureshR.
* Add OAuthGrantType enum for OAuth 2.0 API grant types ([#5161](https://github.com/matrix-org/matrix-js-sdk/pull/5161)). Contributed by @hughns.
* Add support for stable OAuth2.0 aware feature from MSC3824 ([#5159](https://github.com/matrix-org/matrix-js-sdk/pull/5159)). Contributed by @hughns.
* Give RoomWidgetClient the ability to send and receive sticky events ([#5142](https://github.com/matrix-org/matrix-js-sdk/pull/5142)). Contributed by @robintown.
## 🐛 Bug Fixes
* [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated ([#5168](https://github.com/matrix-org/matrix-js-sdk/pull/5168)). Contributed by @toger5.
* [MatrixRTC] Fix delayId not resetting on leave ([#5156](https://github.com/matrix-org/matrix-js-sdk/pull/5156)). Contributed by @toger5.
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
==================================================================================================
## 🦖 Deprecations
* Deprecate unused `EventShieldReason` reason codes ([#5127](https://github.com/matrix-org/matrix-js-sdk/pull/5127)). Contributed by @richvdh.
## ✨ Features
* Add stable m.oauth UIA stage enum ([#5138](https://github.com/matrix-org/matrix-js-sdk/pull/5138)). Contributed by @hughns.
* Add `MatrixEvent.getKeyForwardingUser` ([#5128](https://github.com/matrix-org/matrix-js-sdk/pull/5128)). Contributed by @richvdh.
* Add types for (unstable) policy servers ([#5116](https://github.com/matrix-org/matrix-js-sdk/pull/5116)). Contributed by @turt2live.
## 🐛 Bug Fixes
* [Backport staging] Recalculate room name on loading members ([#5164](https://github.com/matrix-org/matrix-js-sdk/pull/5164)). Contributed by @RiotRobot.
* Avoid rapidly retrying failed requests ([#5146](https://github.com/matrix-org/matrix-js-sdk/pull/5146)). Contributed by @andybalaam.
* [matrixRTC] MatrixRTCSessions, add missing event reemission. ([#5144](https://github.com/matrix-org/matrix-js-sdk/pull/5144)). Contributed by @toger5.
* Use normal base64 encoding for RTC backend identities ([#5129](https://github.com/matrix-org/matrix-js-sdk/pull/5129)). Contributed by @robintown.
* export parseCallNotificationContent and isMyMembership from RTC types ([#5132](https://github.com/matrix-org/matrix-js-sdk/pull/5132)). Contributed by @Half-Shot.
Changes in [40.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.0.0) (2026-01-13)
==================================================================================================
## 🚨 BREAKING CHANGES
* MatrixRTC Pseudonymous livekit identities ([#5110](https://github.com/matrix-org/matrix-js-sdk/pull/5110)). Contributed by @toger5.
## 🦖 Deprecations
* Mark `forwardingCurve25519KeyChain` as deprecated ([#5111](https://github.com/matrix-org/matrix-js-sdk/pull/5111)). Contributed by @richvdh.
* Mark `IEventDecryptionResult` as deprecated ([#5112](https://github.com/matrix-org/matrix-js-sdk/pull/5112)). Contributed by @richvdh.
## ✨ Features
* Implement MSC4387: M\_SAFETY error ([#5107](https://github.com/matrix-org/matrix-js-sdk/pull/5107)). Contributed by @Half-Shot.
* Implement \_unstable\_getRTCTransports for MSC4143 ([#5104](https://github.com/matrix-org/matrix-js-sdk/pull/5104)). Contributed by @Half-Shot.
* Use `membershipID` for session events ([#5105](https://github.com/matrix-org/matrix-js-sdk/pull/5105)). Contributed by @toger5.
## 🐛 Bug Fixes
* Make MatrixRTC encryption key types narrower for TS 5.9 compatibility ([#5117](https://github.com/matrix-org/matrix-js-sdk/pull/5117)). Contributed by @robintown.
* Re-check outgoing requests after processing them ([#5109](https://github.com/matrix-org/matrix-js-sdk/pull/5109)). Contributed by @andybalaam.
* Make token refresher init itself lazily ([#5106](https://github.com/matrix-org/matrix-js-sdk/pull/5106)). Contributed by @dbkr.
Changes in [39.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.4.0) (2025-12-16)
==================================================================================================
## ✨ Features
* Import room key bundles received after invite. ([#5080](https://github.com/matrix-org/matrix-js-sdk/pull/5080)). Contributed by @kaylendog.
## 🐛 Bug Fixes
* Allow msc4354\_sticky\_key to be optional on sticky events. ([#5073](https://github.com/matrix-org/matrix-js-sdk/pull/5073)). Contributed by @Half-Shot.
* Handle all response fields from /context API being optional ([#5089](https://github.com/matrix-org/matrix-js-sdk/pull/5089)). Contributed by @t3chguy.
Changes in [39.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.3.0) (2025-12-02)
==================================================================================================
## 🐛 Bug Fixes
* Re-add truthy check on room name/avatar/alias events ([#5081](https://github.com/matrix-org/matrix-js-sdk/pull/5081)). Contributed by @t3chguy.
* Fix invalid state events corrupting room objects ([#5078](https://github.com/matrix-org/matrix-js-sdk/pull/5078)). Contributed by @t3chguy.
Changes in [39.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.2.0) (2025-11-18)
==================================================================================================
## ✨ Features
* Delayed event management: split endpoints, no auth ([#5066](https://github.com/matrix-org/matrix-js-sdk/pull/5066)). Contributed by @AndrewFerr.
* do not set cache in authenticated fetch ([#5020](https://github.com/matrix-org/matrix-js-sdk/pull/5020)). Contributed by @pkuzco.
## 🐛 Bug Fixes
* Fix media switching during legacy calls ([#5069](https://github.com/matrix-org/matrix-js-sdk/pull/5069)). Contributed by @langleyd.
Changes in [39.1.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.2) (2025-11-04)
==================================================================================================
Re-release of v39.1.0 to fix npm publishing workflow
Changes in [39.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.1) (2025-11-04)
==================================================================================================
Re-release of v39.1.0 to fix npm publishing workflow
Changes in [39.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.0) (2025-11-04)
==================================================================================================
## ✨ Features
* [MatrixRTC] Sticky Events support (MSC4354) ([#5017](https://github.com/matrix-org/matrix-js-sdk/pull/5017)). Contributed by @toger5.
* Add `CryptoApi.getSecretStorageStatus` ([#5054](https://github.com/matrix-org/matrix-js-sdk/pull/5054)). Contributed by @richvdh.
* Add parseCallNotificationContent ([#5015](https://github.com/matrix-org/matrix-js-sdk/pull/5015)). Contributed by @toger5.
* MSC4140: support filters on delayed event lookup ([#5038](https://github.com/matrix-org/matrix-js-sdk/pull/5038)). Contributed by @AndrewFerr.
Changes in [39.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.0.0) (2025-10-21)
==================================================================================================
## 🚨 BREAKING CHANGES
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
## ✨ Features
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
* Implement Sticky Events MSC4354 ([#5028](https://github.com/matrix-org/matrix-js-sdk/pull/5028)). Contributed by @Half-Shot.
* feat(client): allow disabling VoIP support ([#5021](https://github.com/matrix-org/matrix-js-sdk/pull/5021)). Contributed by @pkuzco.
## 🐛 Bug Fixes
* Only use the first 3 viaServers specified ([#5034](https://github.com/matrix-org/matrix-js-sdk/pull/5034)). Contributed by @t3chguy.
* Fetch the user's device info before processing a verification request ([#5030](https://github.com/matrix-org/matrix-js-sdk/pull/5030)). Contributed by @andybalaam.
Changes in [38.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.4.0) (2025-10-07)
==================================================================================================
## ✨ Features
* Add call intent to RTC call notifications ([#5010](https://github.com/matrix-org/matrix-js-sdk/pull/5010)). Contributed by @Half-Shot.
* Implement experimental encrypted state events. ([#4994](https://github.com/matrix-org/matrix-js-sdk/pull/4994)). Contributed by @kaylendog.
## 🐛 Bug Fixes
* Exclude cancelled requests from in-progress lists ([#5016](https://github.com/matrix-org/matrix-js-sdk/pull/5016)). Contributed by @andybalaam.
Changes in [38.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.3.0) (2025-09-23)
==================================================================================================
## 🐛 Bug Fixes
* Remove knocked room when membership changes to join ([#4977](https://github.com/matrix-org/matrix-js-sdk/pull/4977)). Contributed by @svajunas-budrys.
Changes in [38.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0) (2025-09-16)
==================================================================================================
Fix [CVE-2025-59160](https://www.cve.org/CVERecord?id=CVE-2025-59160) / [GHSA-mp7c-m3rh-r56v](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56v)
Changes in [38.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.1.0) (2025-09-09)
==================================================================================================
## ✨ Features
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type ([#4979](https://github.com/matrix-org/matrix-js-sdk/pull/4979)). Contributed by @toger5.
* MatrixRTC: Add RTC decline event ([#4978](https://github.com/matrix-org/matrix-js-sdk/pull/4978)). Contributed by @toger5.
* Make a MatrixRTCSession emit once the RTCNotification is sent ([#4976](https://github.com/matrix-org/matrix-js-sdk/pull/4976)). Contributed by @toger5.
* Use hydra semantics for unknown room versions ([#4957](https://github.com/matrix-org/matrix-js-sdk/pull/4957)). Contributed by @dbkr.
* Expose the StatusChanged event through the RTCSession ([#4974](https://github.com/matrix-org/matrix-js-sdk/pull/4974)). Contributed by @toger5.
* Add `probablyLeft` event to the MatrixRTCSession ([#4962](https://github.com/matrix-org/matrix-js-sdk/pull/4962)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix `m.topic` format ([#4984](https://github.com/matrix-org/matrix-js-sdk/pull/4984)). Contributed by @tulir.
* Fix stable-suffixed MSC4133 support ([#4983](https://github.com/matrix-org/matrix-js-sdk/pull/4983)). Contributed by @dbkr.
* Fix thread edit aggregation race condition ([#4980](https://github.com/matrix-org/matrix-js-sdk/pull/4980)). Contributed by @basnijholt.
Changes in [38.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.0.0) (2025-08-27)
==================================================================================================
## 🚨 BREAKING CHANGES
* Release tranche of breaking changes ([#4975](https://github.com/matrix-org/matrix-js-sdk/pull/4975)).
* Remove support for FetchHttpApi `onlyData = false`
* Remove deprecated `IJoinRoomOpts.syncRoom`
* Remove deprecated methods which are unsupported in rust crypto
* Remove deprecated getAuthIssuer method
* Remove deprecated beginKeyVerification method
* Remove deprecated isEncryptedDisabledForUnverifiedDevices getter
* Remove deprecated UndecryptableToDeviceEvent MatrixClient emit
* Remove deprecated defer utility method
* Remove deprecated UIAResponse dummy type
* Remove deprecated MatrixRTCSession MembershipConfig fields
* Remove deprecated findVerificationRequestDMInProgress and storeSessionBackupPrivateKey methods in favour of overloads
## ✨ Features
* Allow multiple rtc sessions per room (with different sessionDescriptions) ([#4945](https://github.com/matrix-org/matrix-js-sdk/pull/4945)). Contributed by @toger5.
* Add support for login\_hint in authorization url generation ([#4943](https://github.com/matrix-org/matrix-js-sdk/pull/4943)). Contributed by @odelcroi.
* Only process MatrixRTC sessions associated with calls for `callMembershipsForRoom` ([#4960](https://github.com/matrix-org/matrix-js-sdk/pull/4960)). Contributed by @fkwp.
Changes in [37.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.13.0) (2025-08-11)
====================================================================================================
This release supports new v12 Matrix rooms and consequently has a breaking change, removing powerLevelNorm from the RoomMember object as this can't be supported with infinite power levels. Apps should use the non-normalised `powerLevel` instead.
## 🚨 BREAKING CHANGES
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
## ✨ Features
* [Backport staging] Support v12 rooms in maySendEvent ([#4956](https://github.com/matrix-org/matrix-js-sdk/pull/4956)). Contributed by @RiotRobot.
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
* Experimental support for sharing encrypted history on invite ([#4920](https://github.com/matrix-org/matrix-js-sdk/pull/4920)). Contributed by @richvdh.
* Use the logger associated with MatrixClient in rust sdk ([#4918](https://github.com/matrix-org/matrix-js-sdk/pull/4918)). Contributed by @richvdh.
* Update to matrix-sdk-crypto-wasm 15.1.0, and add new `ShieldStateCode.MismatchedSender` ([#4916](https://github.com/matrix-org/matrix-js-sdk/pull/4916)). Contributed by @richvdh.
## 🐛 Bug Fixes
* Fix unknown/broken state in the RTC Membership Manager causing unnecassary error logging. ([#4944](https://github.com/matrix-org/matrix-js-sdk/pull/4944)). Contributed by @toger5.
Changes in [37.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.12.0) (2025-07-29)
====================================================================================================
## 🦖 Deprecations
* Deprecate non-functional `IJoinRoomOpts.syncRoom` ([#4913](https://github.com/matrix-org/matrix-js-sdk/pull/4913)). Contributed by @richvdh.
## ✨ Features
* Custom abort timeout logic for restarting delayed events that is compatible with the widget api ([#4927](https://github.com/matrix-org/matrix-js-sdk/pull/4927)). Contributed by @toger5.
* Allow sending notification events when starting a call ([#4826](https://github.com/matrix-org/matrix-js-sdk/pull/4826)). Contributed by @robintown.
## 🐛 Bug Fixes
* Fix deep import incompatibility (#4924) ([#4925](https://github.com/matrix-org/matrix-js-sdk/pull/4925)). Contributed by @toriningen.
* Fix more incorrect logger use ([#4904](https://github.com/matrix-org/matrix-js-sdk/pull/4904)). Contributed by @richvdh.
Changes in [37.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.11.0) (2025-07-15)
====================================================================================================
## ✨ Features
* Update README to make Element sponsorship explicit ([#4901](https://github.com/matrix-org/matrix-js-sdk/pull/4901)). Contributed by @neilisfragile.
* Use client logger in more places (crypto code) ([#4900](https://github.com/matrix-org/matrix-js-sdk/pull/4900)). Contributed by @richvdh.
* Use client logger in `MatrixRTCSessionManager` ([#4898](https://github.com/matrix-org/matrix-js-sdk/pull/4898)). Contributed by @richvdh.
* Use client logger in more places (core code) ([#4899](https://github.com/matrix-org/matrix-js-sdk/pull/4899)). Contributed by @richvdh.
* crypto: Add new `ClientEvent.ReceivedToDeviceMessage` with proper `OlmEncryptionInfo` support ([#4891](https://github.com/matrix-org/matrix-js-sdk/pull/4891)). Contributed by @BillCarsonFr.
Changes in [37.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.10.0) (2025-07-01)
====================================================================================================
## ✨ Features
* Update matrix-sdk-crypto-wasm to `15.0.0` ([#4882](https://github.com/matrix-org/matrix-js-sdk/pull/4882)). Contributed by @richvdh.
* Allow customizing the IndexedDB database prefix used by Rust crypto. ([#4878](https://github.com/matrix-org/matrix-js-sdk/pull/4878)). Contributed by @clokep.
* Remove `@matrix-org/olm` from dependency list ([#4876](https://github.com/matrix-org/matrix-js-sdk/pull/4876)). Contributed by @richvdh.
* Redact on ban: Client implementation ([#4867](https://github.com/matrix-org/matrix-js-sdk/pull/4867)). Contributed by @turt2live.
Changes in [37.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.9.0) (2025-06-17)
==================================================================================================
## 🐛 Bug Fixes
* Ensure we send spec-compliant filter strings by stripping out null values ([#4865](https://github.com/matrix-org/matrix-js-sdk/pull/4865)). Contributed by @t3chguy.
* Fix MatrixRTC membership manager failing to rejoin in a race condition (sync vs not found response) ([#4861](https://github.com/matrix-org/matrix-js-sdk/pull/4861)). Contributed by @toger5.
* Include extraParams in all HTTP requests ([#4860](https://github.com/matrix-org/matrix-js-sdk/pull/4860)). Contributed by @rsb-tbg.
Changes in [37.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.8.0) (2025-06-10)
==================================================================================================
## 🐛 Bug Fixes
* [Backport staging] Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.2.1 ([#4869](https://github.com/matrix-org/matrix-js-sdk/pull/4869)). Contributed by @RiotRobot.
Changes in [37.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.7.0) (2025-06-03)
==================================================================================================
## 🦖 Deprecations
* MatrixRTC: Rename `MembershipConfig` parameters ([#4714](https://github.com/matrix-org/matrix-js-sdk/pull/4714)). Contributed by @toger5.
## ✨ Features
* Allow the embedded client to work without update\_state support ([#4849](https://github.com/matrix-org/matrix-js-sdk/pull/4849)). Contributed by @robintown.
* Check for `unknown variant` on to-device sending and fall back to room event encryption. ([#4847](https://github.com/matrix-org/matrix-js-sdk/pull/4847)). Contributed by @toger5.
* Reapply "Distinguish room state and timeline events in embedded clients" ([#4790](https://github.com/matrix-org/matrix-js-sdk/pull/4790)). Contributed by @robintown.
Changes in [37.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.6.0) (2025-05-20)
==================================================================================================
## 🦖 Deprecations
* Deprecate utils function `defer` in favour of `Promise.withResolvers` ([#4829](https://github.com/matrix-org/matrix-js-sdk/pull/4829)). Contributed by @t3chguy.
## ✨ Features
* Update to Node 22 LTS ([#4832](https://github.com/matrix-org/matrix-js-sdk/pull/4832)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Fix autodiscovery handling of 2xx (non-200) codes ([#4833](https://github.com/matrix-org/matrix-js-sdk/pull/4833)). Contributed by @t3chguy.
Changes in [37.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.5.0) (2025-05-06)
==================================================================================================
## ✨ Features
* Stabilise MSC3765 ([#4767](https://github.com/matrix-org/matrix-js-sdk/pull/4767)). Contributed by @Johennes.
* Inherit `methodFactory` extensions from the parent to the child loggers. ([#4809](https://github.com/matrix-org/matrix-js-sdk/pull/4809)). Contributed by @toger5.
## 🐛 Bug Fixes
* [Backport staging] Fix token refresh behaviour for non-expired tokens ([#4827](https://github.com/matrix-org/matrix-js-sdk/pull/4827)). Contributed by @RiotRobot.
* Refactor how token refreshing works to be more resilient ([#4819](https://github.com/matrix-org/matrix-js-sdk/pull/4819)). Contributed by @t3chguy.
Changes in [37.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.4.0) (2025-04-22)
==================================================================================================
## ✨ Features
* MatrixRTC: Add combined `toDeviceAndRoomKeyTransport` ([#4792](https://github.com/matrix-org/matrix-js-sdk/pull/4792)). Contributed by @toger5.
* Make logging consistent for matrixRTC ([#4788](https://github.com/matrix-org/matrix-js-sdk/pull/4788)). Contributed by @toger5.
* MatrixRTC: ToDevice distribution for media stream keys ([#4785](https://github.com/matrix-org/matrix-js-sdk/pull/4785)). Contributed by @BillCarsonFr.
## 🐛 Bug Fixes
* Fix token refresh racing with other requests and not using new token ([#4798](https://github.com/matrix-org/matrix-js-sdk/pull/4798)). Contributed by @t3chguy.
* Fix fallback to MemoryCryptoStore when LocalStorage unavailable ([#4797](https://github.com/matrix-org/matrix-js-sdk/pull/4797)). Contributed by @t3chguy.
* Remove duplicate `deleteSecretStorage` in `RustCrypto.resetEncryption` ([#4789](https://github.com/matrix-org/matrix-js-sdk/pull/4789)). Contributed by @florianduros.
* Fix `RustCrypto.resetEncryption` failure ([#4772](https://github.com/matrix-org/matrix-js-sdk/pull/4772)). Contributed by @florianduros.
Changes in [37.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.3.0) (2025-04-08)
==================================================================================================
## ✨ Features
* MatrixRTC MembershipManger: remove redundant sendDelayedEventAction and expose status ([#4747](https://github.com/matrix-org/matrix-js-sdk/pull/4747)). Contributed by @toger5.
* Abstract logout-causing error type from tokenRefreshFunction calls ([#4765](https://github.com/matrix-org/matrix-js-sdk/pull/4765)). Contributed by @t3chguy.
* Improve PushProcessor::getPushRuleGlobRegex ([#4764](https://github.com/matrix-org/matrix-js-sdk/pull/4764)). Contributed by @t3chguy.
* Export push processor \& method for converting matrix glob to regexp ([#4763](https://github.com/matrix-org/matrix-js-sdk/pull/4763)). Contributed by @t3chguy.
* Add authenticated media parameter to getMediaConfig ([#4762](https://github.com/matrix-org/matrix-js-sdk/pull/4762)). Contributed by @m004.
* Rust crypto: set a timeout on outgoing HTTP requests ([#4761](https://github.com/matrix-org/matrix-js-sdk/pull/4761)). Contributed by @richvdh.
* Switch sliding sync support to simplified sliding sync ([#4400](https://github.com/matrix-org/matrix-js-sdk/pull/4400)). Contributed by @dbkr.
Changes in [37.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.2.0) (2025-03-25)
==================================================================================================
## ✨ Features
* Add reportRoom API ([#4753](https://github.com/matrix-org/matrix-js-sdk/pull/4753)). Contributed by @Half-Shot.
* MatrixRTC: New membership manager ([#4726](https://github.com/matrix-org/matrix-js-sdk/pull/4726)). Contributed by @toger5.
* Add disableKeyStorage() to crypto API ([#4742](https://github.com/matrix-org/matrix-js-sdk/pull/4742)). Contributed by @dbkr.
## 🐛 Bug Fixes
* Allow port differing in OIDC dynamic registration URIs ([#4749](https://github.com/matrix-org/matrix-js-sdk/pull/4749)). Contributed by @t3chguy.
* OIDC: only pass logo\_uri, policy\_uri, tos\_uri if they conform to "common base" ([#4748](https://github.com/matrix-org/matrix-js-sdk/pull/4748)). Contributed by @t3chguy.
Changes in [37.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.1.0) (2025-03-11)
==================================================================================================
## 🦖 Deprecations
* MatrixRTC: MembershipManager test cases and deprecation of MatrixRTCSession.room ([#4713](https://github.com/matrix-org/matrix-js-sdk/pull/4713)). Contributed by @toger5.
## ✨ Features
* Add `EventType.SecretRequest` and `EventType.SecretSend` ([#4728](https://github.com/matrix-org/matrix-js-sdk/pull/4728)). Contributed by @richvdh.
* Attest npm package provenance ([#4724](https://github.com/matrix-org/matrix-js-sdk/pull/4724)). Contributed by @t3chguy.
* Report backup key import progress on start and improve types ([#4711](https://github.com/matrix-org/matrix-js-sdk/pull/4711)). Contributed by @ajbura.
## 🐛 Bug Fixes
* Handle unexpected token refresh failures gracefully ([#4731](https://github.com/matrix-org/matrix-js-sdk/pull/4731)). Contributed by @t3chguy.
* Fix idempotency issue around token refresh ([#4730](https://github.com/matrix-org/matrix-js-sdk/pull/4730)). Contributed by @t3chguy.
* Delete the dehydrated device when resetEncryption is called ([#4727](https://github.com/matrix-org/matrix-js-sdk/pull/4727)). Contributed by @uhoreg.
Changes in [37.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.0.0) (2025-02-25)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove deprecated `PrefixedLogger` interface ([#4705](https://github.com/matrix-org/matrix-js-sdk/pull/4705)). Contributed by @richvdh.
* Remove legacy crypto ([#4653](https://github.com/matrix-org/matrix-js-sdk/pull/4653)). Contributed by @florianduros.
## 🦖 Deprecations
* Improve types around User Interactive Auth ([#4709](https://github.com/matrix-org/matrix-js-sdk/pull/4709)). Contributed by @t3chguy.
## ✨ Features
* Fix typos in client.ts ([#4715](https://github.com/matrix-org/matrix-js-sdk/pull/4715)). Contributed by @toger5.
* Enable key upload to backups where we have the decryption key ([#4677](https://github.com/matrix-org/matrix-js-sdk/pull/4677)). Contributed by @ajbura.
* Remove deprecated `PrefixedLogger` interface ([#4705](https://github.com/matrix-org/matrix-js-sdk/pull/4705)). Contributed by @richvdh.
* `MatrixClient.setAccountData`: await remote echo. ([#4695](https://github.com/matrix-org/matrix-js-sdk/pull/4695)). Contributed by @richvdh.
## 🐛 Bug Fixes
* Improve types around User Interactive Auth ([#4709](https://github.com/matrix-org/matrix-js-sdk/pull/4709)). Contributed by @t3chguy.
* Fix `resetEncryption` to remove secrets in 4S ([#4683](https://github.com/matrix-org/matrix-js-sdk/pull/4683)). Contributed by @florianduros.
Changes in [36.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.2.0) (2025-02-11)
==================================================================================================
## 🦖 Deprecations
* [Backport staging] Deprecate parameter and functions using legacy crypto in `models/event.ts` ([#4700](https://github.com/matrix-org/matrix-js-sdk/pull/4700)). Contributed by @RiotRobot.
## ✨ Features
* Improve types around Terms ([#4674](https://github.com/matrix-org/matrix-js-sdk/pull/4674)). Contributed by @t3chguy.
* Provide more options for starting dehydration ([#4664](https://github.com/matrix-org/matrix-js-sdk/pull/4664)). Contributed by @uhoreg.
* Device Dehydration | js-sdk: store/load dehydration key ([#4599](https://github.com/matrix-org/matrix-js-sdk/pull/4599)). Contributed by @BillCarsonFr.
* Add unspecced backup disable flag ([#4661](https://github.com/matrix-org/matrix-js-sdk/pull/4661)). Contributed by @dbkr.
* Switch OIDC primarily to new `/auth_metadata` API ([#4626](https://github.com/matrix-org/matrix-js-sdk/pull/4626)). Contributed by @t3chguy.
* Add `CryptoApi.resetEncryption` ([#4614](https://github.com/matrix-org/matrix-js-sdk/pull/4614)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix topic types ([#4678](https://github.com/matrix-org/matrix-js-sdk/pull/4678)). Contributed by @Half-Shot.
* Handle empty m.room.topic ([#4673](https://github.com/matrix-org/matrix-js-sdk/pull/4673)). Contributed by @Half-Shot.
* `CryptoApi.resetEncryption` should always create a new key backup ([#4648](https://github.com/matrix-org/matrix-js-sdk/pull/4648)). Contributed by @florianduros.
Changes in [36.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.1.0) (2025-01-28)
==================================================================================================
## ✨ Features
* Deprecate `MatrixClient.login` and replace with `loginRequest` ([#4632](https://github.com/matrix-org/matrix-js-sdk/pull/4632)). Contributed by @richvdh.
* Use `SyncCryptoCallback` api instead of legacy crypto in sliding sync ([#4624](https://github.com/matrix-org/matrix-js-sdk/pull/4624)). Contributed by @florianduros.
* Distinguish room state and timeline events in embedded clients ([#4574](https://github.com/matrix-org/matrix-js-sdk/pull/4574)). Contributed by @robintown.
* Allow setting default secret storage key id to null ([#4615](https://github.com/matrix-org/matrix-js-sdk/pull/4615)). Contributed by @florianduros.
* Add authenticated media to getAvatarUrl in room and room-member models ([#4616](https://github.com/matrix-org/matrix-js-sdk/pull/4616)). Contributed by @m004.
* Send MSC3981 'recurse' param on `/relations` endpoint on Matrix 1.10 servers ([#4023](https://github.com/matrix-org/matrix-js-sdk/pull/4023)). Contributed by @dbkr.
## 🐛 Bug Fixes
* [Backport staging] Revert "Distinguish room state and timeline events in embedded clients (#4574)" ([#4657](https://github.com/matrix-org/matrix-js-sdk/pull/4657)). Contributed by @RiotRobot.
* Change randomString et al to be secure ([#4621](https://github.com/matrix-org/matrix-js-sdk/pull/4621)). Contributed by @dbkr.
* Fix issue with sentinels being incorrect on m.room.member events ([#4609](https://github.com/matrix-org/matrix-js-sdk/pull/4609)). Contributed by @t3chguy.
Changes in [36.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.0.0) (2025-01-14)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership ([#4583](https://github.com/matrix-org/matrix-js-sdk/pull/4583)). Contributed by @toger5.
## ✨ Features
* MatrixRTC: Implement expiry logic for CallMembership and additional test coverage ([#4587](https://github.com/matrix-org/matrix-js-sdk/pull/4587)). Contributed by @toger5.
## 🐛 Bug Fixes
* Don't retry on 4xx responses ([#4601](https://github.com/matrix-org/matrix-js-sdk/pull/4601)). Contributed by @dbkr.
* Upgrade matrix-sdk-crypto-wasm to 12.1.0 ([#4596](https://github.com/matrix-org/matrix-js-sdk/pull/4596)). Contributed by @andybalaam.
* Avoid key prompts when resetting crypto ([#4586](https://github.com/matrix-org/matrix-js-sdk/pull/4586)). Contributed by @dbkr.
* Handle when aud OIDC claim is an Array ([#4584](https://github.com/matrix-org/matrix-js-sdk/pull/4584)). Contributed by @liamdiprose.
* Save the key backup key to 4S during `bootstrapSecretStorage ` ([#4542](https://github.com/matrix-org/matrix-js-sdk/pull/4542)). Contributed by @dbkr.
* Only re-prepare MatrixrRTC delayed disconnection event on 404 ([#4575](https://github.com/matrix-org/matrix-js-sdk/pull/4575)). Contributed by @toger5.
Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18)
==================================================================================================
This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage.
## 🐛 Bug Fixes
* Upgrade matrix-sdk-crypto-wasm to 1.11.0 ([#4593](https://github.com/matrix-org/matrix-js-sdk/pull/4593)).
Changes in [35.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.0.0) (2024-12-17)
==================================================================================================
## 🚨 BREAKING CHANGES
+239 -1
View File
@@ -1,3 +1,241 @@
# Contributing code to matrix-js-sdk
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
## How to contribute
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Your PR should have a title that describes what change is being made. This
is used for the text in the Changelog entry by default (see below), so a good
title will tell a user succinctly what change is being made. "Fix bug where
cows had five legs" and, "Add support for miniature horses" are examples of good
titles. Don't include an issue number here: that belongs in the description.
Definitely don't use the GitHub default of "Update file.ts".
As for your PR description, it should include these things:
- References to any bugs fixed by the change (in GitHub's `Fixes` notation)
- Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch. This information is
also helpful when we come back to look at this in 6 months and ask "why did
we do it like that?" we have a chance of finding out.
- Why didn't it work before? Why does it work now? What use cases does it
unlock?
- If you find yourself adding information on how the code works or why you
chose to do it the way you did, make sure this information is instead
written as comments in the code itself.
- Sometimes a PR can change considerably as it is developed. In this case,
the description should be updated to reflect the most recent state of
the PR. (It can be helpful to retain the old content under a suitable
heading, for additional context.)
- Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
- Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
### Changelogs
There's no need to manually add Changelog entries: we use information in the
pull request to populate the information that goes into the changelogs our
users see, both for Element Web itself and other projects on which it is based.
This is picked up from both labels on the pull request and the `Notes:`
annotation in the description. By default, the PR title will be used for the
changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog:
_Fix llama herding bug_
```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
```
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`):
_Remove outdated comment from `Ungulates.ts`_
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
_Fix another herding bug_
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
- element-web
- element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to
migrate:
_Remove legacy class_
```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
```
Other metadata can be added using labels.
- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump.
- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump.
- `T-Defect`: A bug fix (in either code or docs).
- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
## Tests
Your PR should include tests.
For new user facing features in `matrix-js-sdk`, you
must include comprehensive unit tests written in Vitest.
The existing tests can be found under `spec/unit`
It's good practice to write tests alongside the code as it ensures the code is testable from
the start, and gives you a fast feedback loop while you're developing the
functionality. Unit tests are necessary even for bug fixes.
When writing unit tests, please aim for a high level of test coverage
for new code - 80% or greater. If you cannot achieve that, please document
why it's not possible in your PR.
Tests validate that your change works as intended and also document
concisely what is being changed. Ideally, your new tests fail
prior to your change, and succeed once it has been applied. You may
find this simpler to achieve if you write the tests first.
If you're spiking some code that's experimental and not being used to support
production features, exceptions can be made to requirements for tests.
Note that tests will still be required in order to ship the feature, and it's
strongly encouraged to think about tests early in the process, as adding
tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
## Code style
Code style is documented in [code_style.md](./code_style.md).
Contributors are encouraged to it and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and **_never_** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
## Sign off
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
# Review expectations
See https://github.com/vector-im/element-meta/wiki/Review-process
# Merge Strategy
The preferred method for merging pull requests is squash merging to keep the
commit history trim, but it is up to the discretion of the team member merging
the change. We do not support rebase merges due to `allchange` being unable to
handle them. When merging make sure to leave the default commit title, or
at least leave the PR number at the end in brackets like by default.
When stacking pull requests, you may wish to do the following:
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
+28 -12
View File
@@ -11,6 +11,22 @@
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
browser or in Node.js.
---
<picture>
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
<img src="contrib/element-logo-fallback.png" alt="Element logo">
</picture>
<br>
Development and maintenance is proudly sponsored by [Element](https://element.io). Element uses the SDK in their flagship [web](https://github.com/element-hq/element-web) and [desktop](https://github.com/element-hq/element-desktop) clients.
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
---
#### Minimum Matrix server version: v1.1
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
@@ -25,10 +41,10 @@ endpoints from before Matrix 1.1, for example.
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
`yarn add matrix-js-sdk`
`pnpm add matrix-js-sdk`
```javascript
import * as sdk from "matrix-js-sdk";
@@ -154,7 +170,7 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed),
or in browser applications, via a bundler such as Webpack or Vite.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officially supported.
## Emitted events
@@ -294,7 +310,7 @@ This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. Yo
host the API reference from the source files like this:
```
$ yarn gendoc
$ pnpm gendoc
$ cd docs
$ python -m http.server 8005
```
@@ -307,8 +323,6 @@ Then visit `http://localhost:8005` to see the API docs.
## Initialization
**Do not use `matrixClient.initLegacyCrypto()`. This method is deprecated and no longer maintained.**
To initialize the end-to-end encryption support in the matrix client:
```javascript
@@ -323,6 +337,8 @@ const matrixClient = sdk.createClient({
await matrixClient.initRustCrypto();
```
Note that by default it will attempt to use the Indexed DB provided by the browser as a crypto store. If running outside the browser, you will need to pass [an options object](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#initrustcrypto) which includes `useIndexedDB: false`, to use an ephemeral in-memory store instead. Note that without a persistent store, you'll need to create a new device on the server side (with [`MatrixClient.loginRequest`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#loginrequest)) each time your application starts.
After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto).
**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time.
@@ -437,7 +453,7 @@ want to use this SDK, skip this section._
First, you need to pull in the right build tools:
```
$ yarn install
$ pnpm install
```
## Building
@@ -445,17 +461,17 @@ First, you need to pull in the right build tools:
To build a browser version from scratch when developing:
```
$ yarn build
$ pnpm build
```
To run tests (Jest):
To run tests:
```
$ yarn test
$ pnpm test
```
To run linting:
```
$ yarn lint
$ pnpm lint
```
+3 -10
View File
@@ -7,25 +7,18 @@ module.exports = {
targets: {
esmodules: true,
},
// We want to output ES modules for the final build (mostly to ensure that
// async imports work correctly). However, jest doesn't support ES modules very
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
// when testing.
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
modules: false,
},
],
[
"@babel/preset-typescript",
{
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
// `.js` during transpilation, *except* when we are targetting Jest.
rewriteImportExtensions: process.env.NODE_ENV !== "test",
rewriteImportExtensions: true,
},
],
],
plugins: [
["@babel/plugin-proposal-decorators", { version: "2023-11" }],
"@babel/plugin-transform-numeric-separator",
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-object-rest-spread",
+284
View File
@@ -0,0 +1,284 @@
## Guiding principles
1. We want the lint rules to feel natural for most team members. No one should have to think too much
about the linter.
2. We want to stay relatively close to [industry standards](https://google.github.io/styleguide/tsguide.html)
to make onboarding easier.
3. We describe what good code looks like rather than point out bad examples. We do this to avoid
excessively punishing people for writing code which fails the linter.
4. When something isn't covered by the style guide, we come up with a reasonable rule rather than
claim that it "passes the linter". We update the style guide and linter accordingly.
5. While we aim to improve readability, understanding, and other aspects of the code, we deliberately
do not let solely our personal preferences drive decisions.
6. We aim to have an understandable guide.
## Coding practices
1. Lint rules enforce decisions made by this guide. The lint rules and this guide are kept in
perfect sync.
2. Commit messages are descriptive for the changes. When the project supports squash merging,
only the squashed commit needs to have a descriptive message.
3. When there is disagreement with a code style approved by the linter, a PR is opened against
the lint rules rather than making exceptions on the responsible code PR.
4. Rules which are intentionally broken (via eslint-ignore, @ts-ignore, etc) have a comment
included in the immediate vicinity for why. Determination of whether this is valid applies at
code review time.
5. When editing a file, nearby code is updated to meet the modern standards. "Nearby" is subjective,
but should be whatever is reasonable at review time. Such an example might be to update the
class's code style, but not the file's.
1. These changes should be minor enough to include in the same commit without affecting a code
reviewer's job.
## All code
Unless otherwise specified, the following applies to all code:
1. Files must be formatted with Prettier.
2. 120 character limit per line. Match existing code in the file if it is using a lower guide.
3. A tab/indentation is 4 spaces.
4. Newlines are Unix.
5. A file has a single empty line at the end.
6. Lines are trimmed of all excess whitespace, including blank lines.
7. Long lines are broken up for readability.
## TypeScript / JavaScript
1. Write TypeScript. Turn JavaScript into TypeScript when working in the area.
2. Use [TSDoc](https://tsdoc.org/) to document your code. See [Comments](#comments) below.
3. Use named exports.
4. Use semicolons for block/line termination.
1. Except when defining interfaces, classes, and non-arrow functions specifically.
5. When a statement's body is a single line, it must be written without curly braces, so long as the body is placed on
the same line as the statement.
```typescript
if (x) doThing();
```
6. Blocks for `if`, `for`, `switch` and so on must have a space surrounding the condition, but not
within the condition.
```typescript
if (x) {
doThing();
}
```
7. lowerCamelCase is used for function and variable naming.
8. UpperCamelCase is used for general naming.
9. Interface names should not be marked with an uppercase `I`.
10. One variable declaration per line.
11. If a variable is not receiving a value on declaration, its type must be defined.
```typescript
let errorMessage: string;
```
12. Objects can use shorthand declarations, including mixing of types.
```typescript
{
room,
prop: this.prop,
}
// ... or ...
{ room, prop: this.prop }
```
13. Object keys should always be non-strings when possible.
```typescript
{
property: "value",
"m.unavoidable": true,
[EventType.RoomMessage]: true,
}
```
14. If a variable's type should be boolean, make sure it really is one.
```typescript
const isRealUser = !!userId && ...; // good
const isRealUser = Boolean(userId) && Boolean(userName); // also good
const isRealUser = Boolean(userId) && isReal; // also good (where isReal is another boolean variable)
const isRealUser = Boolean(userId && userName); // also fine
const isRealUser = Boolean(userId || userName); // good: same as &&
const isRealUser = userId && ...; // bad: isRealUser is userId's type, not a boolean
if (userId) // fine: userId is evaluated for truthiness, not stored as a boolean
```
15. Use `switch` statements when checking against more than a few enum-like values.
16. Use `const` for constants, `let` for mutability.
17. Describe types exhaustively (ensure noImplictAny would pass).
1. Notable exceptions are arrow functions used as parameters, when a void return type is
obvious, and when declaring and assigning a variable in the same line.
18. Declare member visibility (public/private/protected).
19. Private members are private and not prefixed unless required for naming conflicts.
1. Convention is to use an underscore or the word "internal" to denote conflicted member names.
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
1. Note that an explicit type is optional if not expected to be used outside of the function call,
unlike in this example:
```typescript
interface MyObject {
hasString: boolean;
}
type Options = MyObject | string;
function doThing(arg: Options) {
// ...
}
```
22. Variables/properties which are `public static` should also be `readonly` when possible.
23. Interface and type properties are terminated with semicolons, not commas.
24. Prefer arrow formatting when declaring functions for interfaces/types:
```typescript
interface Test {
myCallback: (arg: string) => Promise<void>;
}
```
25. Prefer a type definition over an inline type. For example, define an interface.
26. Always prefer to add types or declare a type over the use of `any`. Prefer inferred types
when they are not `any`.
1. When using `any`, a comment explaining why must be present.
27. `import` should be used instead of `require`, as `require` does not have types.
28. Export only what can be reused.
29. Prefer a type like `X | null` instead of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should
supply the room ID if it knows it, otherwise deliberately acknowledge that it doesn't
have one with `null`.
```typescript
function doThingWithRoom(
thing: string,
room: string | null, // require the caller to specify
) {
// ...
}
```
30. There should be approximately one interface, class, or enum per file unless the file is named
"types.ts", "global.d.ts", or ends with "-types.ts".
1. The file name should match the interface, class, or enum name.
31. Bulk functions can be declared in a single file, though named as "foo-utils.ts" or "utils/foo.ts".
32. Imports are grouped by external module imports first, then by internal imports.
33. File ordering is not strict, but should generally follow this sequence:
1. Licence header
2. Imports
3. Constants
4. Enums
5. Interfaces
6. Functions
7. Classes
1. Public/protected/private static properties
2. Public/protected/private properties
3. Constructors
4. Public/protected/private getters & setters
5. Protected and abstract functions
6. Public/private functions
7. Public/protected/private static functions
34. Variable names should be noticeably unique from their types. For example, "str: string" instead
of "string: string".
35. Use double quotes to enclose strings. You may use single quotes if the string contains double quotes.
```typescript
const example1 = "simple string";
const example2 = 'string containing "double quotes"';
```
36. Prefer async-await to promise-chaining
```typescript
async function () {
const result = await anotherAsyncFunction();
// ...
}
```
37. Avoid functions whose fundamental behaviour varies with different parameter types.
Multiple return types are fine, but if the function's behaviour is going to change significantly,
have two separate functions. For example, `SDKConfig.get()` with a string param which returns the
type according to the param given is ok, but `SDKConfig.get()` with no args returning the whole
config object would not be: this should just be a separate function.
## Tests
1. Tests must be written in TypeScript.
2. Mocks are declared below imports, but above everything else.
3. Use the following convention template:
```typescript
// Describe the class, component, or file name.
describe("FooComponent", () => {
// all test inspecific variables go here
beforeEach(() => {
// exclude if not used.
});
afterEach(() => {
// exclude if not used.
});
// Use "it should..." terminology
it("should call the correct API", async () => {
// test-specific variables go here
// function calls/state changes go here
// expectations go here
});
});
// If the file being tested is a utility class:
describe("foo-utils", () => {
describe("firstUtilFunction", () => {
it("should...", async () => {
// ...
});
});
describe("secondUtilFunction", () => {
it("should...", async () => {
// ...
});
});
});
```
## Comments
1. As a general principle: be liberal with comments. This applies to all files: stylesheets as well as
JavaScript/TypeScript.
Good comments not only help future readers understand and maintain the code; they can also encourage good design
by clearly setting out how different parts of the codebase interact where that would otherwise be implicit and
subject to interpretation.
2. Aim to document all types, methods, class properties, functions, etc, with [TSDoc](https://tsdoc.org/) doc comments.
This is _especially_ important for public interfaces in `matrix-js-sdk`, but is good practice in general.
Even very simple interfaces can often benefit from a doc-comment, both as a matter of consistency, and because simple
interfaces have a habit of becoming more complex over time.
3. Inside a function, there is no need to comment every line, but consider:
- before a particular multiline section of code within the function, give an overview of what it does,
to make it easier for a reader to follow the flow through the function as a whole.
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
on how this function interacts with other parts of the codebase.
4. When making changes to existing code, authors are expected to read existing comments and make any necessary changes
to ensure they remain accurate.
5. Reviewers are encouraged to consider whether more comments would be useful, and to ask the author to add them.
It is natural for an author to feel that the code they have just written is "obvious" and that comments would be
redundant, whereas in reality it would take some time for reader unfamiliar with the code to understand it. A
reviewer is well-placed to make a more objective judgement.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+9 -15
View File
@@ -94,20 +94,14 @@ rl.on("line", function (line) {
);
} else if (line.indexOf("/file ") === 0) {
var filename = line.split(" ")[1].trim();
var stream = fs.createReadStream(filename);
matrixClient
.uploadContent({
stream: stream,
name: filename,
})
.then(function (url) {
var content = {
msgtype: MsgType.File,
body: filename,
url: JSON.parse(url).content_uri,
};
matrixClient.sendMessage(viewingRoom.roomId, content);
let buffer = fs.readFileSync("./your_file_name");
matrixClient.uploadContent(new Blob([buffer])).then(function (response) {
matrixClient.sendMessage(viewingRoom.roomId, {
msgtype: MsgType.File,
body: filename,
url: response.content_uri,
});
});
} else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
printMessages();
@@ -167,7 +161,7 @@ matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
if (!viewingRoom || viewingRoom.roomId !== room.roomId) {
if (!viewingRoom || viewingRoom.roomId !== room?.roomId) {
return; // not viewing a room or viewing the wrong room.
}
printLine(event);
@@ -386,7 +380,7 @@ function print(str, formatter) {
}
console.log.apply(console.log, newArgs);
} else {
console.log.apply(console.log, arguments);
console.log.apply(console.log, [...arguments]);
}
}
+7
View File
@@ -9,5 +9,12 @@
"dependencies": {
"cli-color": "^1.0.0",
"matrix-js-sdk": "^34.5.0"
},
"devDependencies": {
"@types/cli-color": "^2.0.6",
"typescript": "^5.6.2"
},
"engines": {
"node": ">=20.0.0"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"esModuleInterop": true,
"noImplicitAny": false,
"noEmit": true,
"skipLibCheck": true,
"allowJs": true,
"checkJs": true,
"strict": true
},
"include": ["app.js"]
}
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
yarn lint
pnpm lint
-47
View File
@@ -1,47 +0,0 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Config } from "jest";
import { env } from "process";
const config: Config = {
testEnvironment: "node",
testMatch: ["<rootDir>/spec/**/*.spec.{js,ts}"],
setupFilesAfterEnv: ["<rootDir>/spec/setupTests.ts"],
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
coverageReporters: ["text-summary", "lcov"],
testResultsProcessor: "@casualbot/jest-sonar-reporter",
// Always print out a summary if there are any failing tests. Normally
// a summary is only printed if there are more than 20 test *suites*.
reporters: [["default", { summaryThreshold: 0 }]],
};
// if we're running under GHA, enable the GHA reporter
if (env["GITHUB_ACTIONS"] !== undefined) {
const reporters: Config["reporters"] = [
["github-actions", { silent: false }],
// as above: always show a summary if there were any failing tests.
["summary", { summaryThreshold: 0 }],
];
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
reporters.push("<rootDir>/spec/slowReporter.cjs");
}
config.reporters = reporters;
}
export default config;
+15 -9
View File
@@ -1,16 +1,26 @@
import { KnipConfig } from "knip";
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
process.env.GITHUB_ACTIONS = "1";
export default {
entry: [
"src/index.ts",
"src/types.ts",
"src/browser-index.ts",
"src/indexeddb-worker.ts",
"src/crypto-api/index.ts",
"src/testing.ts",
"src/matrix.ts",
"src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
"scripts/**",
"spec/**",
"release.sh",
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
"src/**",
// XXX: these should be re-exported by one of the supported exports
"src/matrixrtc/index.ts",
"src/sliding-sync.ts",
"src/webrtc/groupCall.ts",
"src/webrtc/stats/media/mediaTrackStats.ts",
"src/rendezvous/RendezvousChannel.ts",
],
project: ["**/*.{js,ts}"],
ignore: ["examples/**"],
@@ -21,16 +31,12 @@ export default {
"husky",
// Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest",
// Used by jest
"jest-environment-jsdom",
"babel-jest",
"ts-node",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
],
ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml`
"dist",
],
ignoreExportsUsedInFile: true,
includeEntryExports: false,
exclude: ["enumMembers"],
} satisfies KnipConfig;
+60 -56
View File
@@ -1,32 +1,30 @@
{
"name": "matrix-js-sdk",
"version": "35.0.0",
"version": "41.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
"node": ">=22.0.0"
},
"scripts": {
"prepack": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"clean": "rimraf lib",
"build": "yarn build:dev",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"prepare": "pnpm build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
"build": "pnpm build:compile && pnpm build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile": "babel --delete-dir-on-start -d lib --verbose --extensions \".ts,.js\" src",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
"test": "vitest run",
"test:watch": "vitest watch",
"coverage": "pnpm test --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
"url": "git+https://github.com/matrix-org/matrix-js-sdk.git"
},
"keywords": [
"matrix-org"
@@ -50,20 +48,18 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0",
"@matrix-org/olm": "3.2.15",
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.10.0",
"matrix-widget-api": "^1.16.1",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "11"
"p-retry": "8",
"sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@action-validator/cli": "^0.6.0",
@@ -72,6 +68,7 @@
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-decorators": "^7.25.9",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-class-properties": "^7.12.1",
"@babel/plugin-transform-numeric-separator": "^7.12.7",
@@ -79,56 +76,63 @@
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@fetch-mock/vitest": "^0.2.18",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@stylistic/eslint-plugin": "^2.9.0",
"@types/bs58": "^4.0.1",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/jest": "^29.0.0",
"@types/node": "18",
"@types/node": "22",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "10",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.0.0",
"@vitest/coverage-v8": "^4.0.17",
"@vitest/eslint-plugin": "^1.6.6",
"@vitest/ui": "^4.0.17",
"babel-plugin-search-and-replace": "^1.1.1",
"debug": "^4.3.4",
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-config-prettier": "^10.0.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "^2.0.1",
"eslint-plugin-jsdoc": "^62.0.0",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-tsdoc": "^0.5.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.5",
"fetch-mock-jest": "^1.5.1",
"fetch-mock": "^12.6.0",
"happy-dom": "^20.1.0",
"husky": "^9.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"knip": "^5.0.0",
"lint-staged": "^15.0.2",
"knip": "^6.0.0",
"lint-staged": "^16.0.0",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.4.1",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.27.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^4.0.0",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.4.2"
"prettier": "3.8.3",
"typedoc": "^0.28.1",
"typedoc-plugin-coverage": "^4.0.0",
"typedoc-plugin-mdn-links": "^5.0.0",
"typedoc-plugin-missing-exports": "^4.0.0",
"typescript": "^6.0.0",
"vitest": "^4.0.17",
"vitest-sonar-reporter": "^3.0.0"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
}
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint": "8"
}
},
"allowedDeprecatedVersions": {
"eslint": "8"
},
"overrides": {
"expect": "30.3.0",
"flatted@<=3.4.1": "^3.4.2",
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
"vite": "8.0.8"
}
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}
+8353
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
nodeLinker: hoisted
+1 -1
View File
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
sonar.testExecutionReportPaths=coverage/sonar-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
+11 -19
View File
@@ -17,29 +17,29 @@ limitations under the License.
*/
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
/* eslint-disable jest/no-standalone-expect */
// load olm before the sdk if possible
import "./olm-loader";
/* eslint-disable @vitest/no-standalone-expect */
import MockHttpBackend from "matrix-mock-request";
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
import { logger } from "../src/logger";
import { syncPromise } from "./test-utils/test-utils";
import { createClient, IStartClientOpts } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi";
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
import { ISyncResponder } from "./test-utils/SyncResponder";
import { createClient, type IStartClientOpts } from "../src/matrix";
import {
type ICreateClientOpts,
type IDownloadKeyResult,
type MatrixClient,
PendingEventOrdering,
} from "../src/client";
import { type IKeysUploadResponse, type IUploadKeysRequest } from "../src/client";
import { type ISyncResponder } from "./test-utils/SyncResponder";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
* Instead, construct a MatrixClient manually, use fetch-mock to intercept the HTTP requests, and
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
*/
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
@@ -55,10 +55,6 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
sessionStoreBackend?: Storage,
options?: Partial<ICreateClientOpts>,
) {
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
}
this.httpBackend = new MockHttpBackend();
const fullOptions: ICreateClientOpts = {
@@ -69,10 +65,6 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
fetchFn: this.httpBackend.fetchFn as typeof globalThis.fetch,
...options,
};
if (!fullOptions.cryptoStore) {
// expose this so the tests can get to it
fullOptions.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
}
this.client = createClient(fullOptions);
this.deviceKeys = null;
+42 -65
View File
@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import debug from "debug";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { syncPromise } from "../../test-utils/test-utils";
import { type AuthDict, createClient, DebugLogger, type MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { type CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import {
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
@@ -37,6 +38,7 @@ import {
import * as testData from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { CryptoEvent } from "../../../src/crypto-api";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -54,11 +56,7 @@ const TEST_DEVICE_ID = "xzcvb";
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
describe("cross-signing", () => {
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
@@ -76,7 +74,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
function createCryptoCallbacks(): CryptoCallbacks {
return {
getSecretStorageKey: (keys, name) => {
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
},
};
}
@@ -85,7 +83,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
@@ -94,6 +91,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
logger: new DebugLogger(debug(`matrix-js-sdk:cross-signing`)),
});
syncResponder = new SyncResponder(homeserverUrl);
@@ -107,15 +105,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
body: { errcode: "M_NOT_FOUND" },
});
await initCrypto(aliceClient);
await aliceClient.initRustCrypto();
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
afterEach(async () => {
await aliceClient.stopClient();
fetchMock.mockReset();
aliceClient.stopClient();
});
/**
@@ -138,31 +135,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// check the cross-signing keys upload
expect(fetchMock.called("upload-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
// check that the cross-signing keys have been uploaded
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
const keysBody = JSON.parse(keysOpts!.body as string);
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
// there should be a key of each type
// master key is signed by the device
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
expect(keysBody).toHaveProperty(["master_key", "signatures", TEST_USER_ID, `ed25519:${TEST_DEVICE_ID}`]);
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
// ssk and usk are signed by the master key
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(["self_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
// check the publish call
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
const body = JSON.parse(sigsOpts!.body as string);
// there should be a signature for our device, by our self-signing key.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
});
newBackendOnly("get cross signing keys from secret storage and import them", async () => {
it("get cross signing keys from secret storage and import them", async () => {
// Return public cross signing keys
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
@@ -226,9 +221,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
await aliceClient.startClient();
await syncPromise(aliceClient);
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
@@ -241,13 +233,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
const body = JSON.parse(sigsOpts!.body as string);
// the device should have a signature with the public self cross signing keys.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
);
expect(body).toHaveProperty([
TEST_USER_ID,
TEST_DEVICE_ID,
"signatures",
TEST_USER_ID,
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
]);
});
it("can bootstrapCrossSigning twice", async () => {
@@ -259,11 +255,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
// a second call should do nothing except GET requests
fetchMock.mockClear();
await bootstrapCrossSigning(authDict);
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
expect(calls.length).toEqual(0);
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
});
newBackendOnly("will upload existing cross-signing keys to an established secret storage", async () => {
it("will upload existing cross-signing keys to an established secret storage", async () => {
// This rather obscure codepath covers the case that:
// - 4S is set up and working
// - our device has private cross-signing keys, but has not published them to 4S
@@ -271,8 +266,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
mockSetupCrossSigningRequests();
const accountDataAccumulator = new AccountDataAccumulator();
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
accountDataAccumulator.interceptGetAccountData();
const authDict = { type: "test" };
@@ -286,7 +280,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
});
// Prepare for the cross-signing keys
const p = accountDataAccumulator.interceptSetAccountData(":type(m.cross_signing..*)");
const p = accountDataAccumulator.waitForAccountData("m.cross_signing.master");
await bootstrapCrossSigning(authDict);
await p;
@@ -407,7 +401,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
}, 10000);
});
describe("getCrossSigningKeyId", () => {
@@ -419,20 +413,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
*/
function awaitCrossSigningKeysUpload() {
return new Promise<any>((resolve) => {
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
(url, options) => {
const content = JSON.parse(options.body as string);
fetchMock.modifyRoute("upload-cross-signing-keys", {
response: (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return {};
},
// Override the routes define in `mockSetupCrossSigningRequests`
{ overwriteRoutes: true },
);
});
});
}
@@ -463,9 +450,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
describe("crossSignDevice", () => {
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// make sure that there is another device which we can sign
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
@@ -475,17 +459,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
await aliceClient.startClient();
await syncPromise(aliceClient);
// Wait for legacy crypto to find the device
await jest.advanceTimersByTimeAsync(10);
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([aliceClient.getSafeUserId()]);
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
});
afterEach(async () => {
jest.useRealTimers();
});
it("fails for an unknown device", async () => {
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
});
@@ -498,9 +475,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
// check that a sig for the device was uploaded
const calls = fetchMock.calls("upload-sigs");
const calls = fetchMock.callHistory.calls("upload-sigs");
expect(calls.length).toEqual(1);
const body = JSON.parse(calls[0][1]!.body as string);
const body = JSON.parse(calls[0].options!.body as string);
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
expect(deviceSig).toHaveProperty("signatures");
});
File diff suppressed because it is too large Load Diff
+93 -35
View File
@@ -15,17 +15,21 @@ limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type CallLog } from "fetch-mock";
import debug from "debug";
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
import { CryptoEvent } from "../../../src/crypto-api/index";
import { type RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { type AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { emitPromise, EventCounter } from "../../test-utils/test-utils";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -36,10 +40,17 @@ describe("Device dehydration", () => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
},
logger: new DebugLogger(debug(`matrix-js-sdk:dehydration`)),
});
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
// count the number of times the dehydration key gets set
let setDehydrationCount = 0;
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
@@ -49,53 +60,67 @@ describe("Device dehydration", () => {
});
const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;
// start dehydration -- we start with no dehydrated device, and we
// store the dehydrated device that we create
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
fetchMock.get(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
},
});
{ name: "get-dehydrated-device" },
);
let dehydratedDeviceBody: any;
let dehydrationCount = 0;
let resolveDehydrationPromise: () => void;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
});
fetchMock.put(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
(callLog) => {
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
},
{ name: "put-dehydrated-device" },
);
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
expect(creationEventCounter.counter).toEqual(1);
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
// a week later, we should have created another dehydrated device
const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve;
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
expect(dehydrationCount).toEqual(2);
expect(creationEventCounter.counter).toEqual(2);
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
// a new dehydration key will be set
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
fetchMock.modifyRoute("get-dehydrated-device", {
response: {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
},
});
const eventsResponse = jest.fn((url, opts) => {
const eventsResponse = vi.fn((callLog: CallLog) => {
// rehydrating should make two calls to the /events endpoint.
// The first time will return a single event, and the second
// time will return no events (which will signal to the
// rehydration function that it can stop)
const body = JSON.parse(opts.body as string);
const body = JSON.parse(callLog.options.body as string);
const nextBatch = body.next_batch ?? "0";
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
return {
@@ -113,6 +138,41 @@ describe("Device dehydration", () => {
expect(setDehydrationCount).toEqual(2);
expect(eventsResponse.mock.calls).toHaveLength(2);
expect(rehydrationStartedCounter.counter).toEqual(1);
expect(rehydrationCompletedCounter.counter).toEqual(1);
expect(creationEventCounter.counter).toEqual(3);
expect(rehydrationProgressCounter.counter).toEqual(1);
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
// test that if we get an error when we try to rotate, it emits an event
fetchMock.modifyRoute("put-dehydrated-device", {
response: {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
},
});
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await rotationErrorEventPromise;
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
fetchMock.modifyRoute("get-dehydrated-device", {
response: {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
},
});
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
await crypto.startDehydration(true);
await rehydrationErrorEventPromise;
matrixClient.stopClient();
});
});
@@ -133,11 +193,9 @@ async function initializeSecretStorage(
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
const accountData: Map<string, object> = new Map();
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = callLog.url.split("/").pop()!;
const value = accountData.get(name);
if (value) {
return value;
@@ -151,9 +209,9 @@ async function initializeSecretStorage(
};
}
});
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = JSON.parse(opts.body as string);
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = callLog.url.split("/").pop()!;
const value = JSON.parse(callLog.options.body as string);
accountData.set(name, value);
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
return {};
@@ -172,8 +230,8 @@ async function initializeSecretStorage(
privateKey: new Uint8Array(32),
};
}
await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true });
await matrixClient.bootstrapSecretStorage({
await matrixClient.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: true });
await matrixClient.getCrypto()!.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
File diff suppressed because it is too large Load Diff
+174 -293
View File
@@ -14,38 +14,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { Mocked } from "jest-mock";
import { type Mocked } from "vitest";
import {
createClient,
Crypto,
encodeBase64,
ICreateClientOpts,
IEvent,
IMegolmSessionData,
MatrixClient,
type IContent,
type ICreateClientOpts,
type IEvent,
type IMegolmSessionData,
type MatrixClient,
TypedEventEmitter,
} from "../../../src";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import {
advanceTimersUntil,
awaitDecryption,
CRYPTO_BACKENDS,
InitCrypto,
syncPromise,
} from "../../test-utils/test-utils";
import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { flushPromises } from "../../test-utils/flushPromises";
import { defer, IDeferred } from "../../../src/utils";
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent } from "../../../src/crypto-api";
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
import {
decodeRecoveryKey,
DecryptionFailureCode,
CryptoEvent,
type CryptoApi,
DecryptionKeyDoesNotMatchError,
} from "../../../src/crypto-api";
import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -77,10 +76,11 @@ function mockUploadEmitter(
expectedVersion: string,
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
const emitter = new TypedEventEmitter();
fetchMock.removeRoute("mock-upload-emitter");
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const version = new URLSearchParams(new URL(url).search).get("version");
(callLog) => {
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version != expectedVersion) {
return {
status: 403,
@@ -91,7 +91,7 @@ function mockUploadEmitter(
},
};
}
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
@@ -107,21 +107,12 @@ function mockUploadEmitter(
},
};
},
{
overwriteRoutes: true,
},
{ name: "mock-upload-emitter" },
);
return emitter;
}
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend === "libolm" ? test.skip : test;
const isNewBackend = backend === "rust-sdk";
describe("megolm-keys backup", () => {
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
let syncResponder: SyncResponder;
@@ -132,12 +123,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
let e2eKeyResponder: E2EKeyResponder;
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockInitialApiRequests(TEST_HOMESERVER_URL);
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
@@ -148,15 +137,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
aliceClient?.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
await vi.runAllTimersAsync();
fetchMock.mockReset();
jest.restoreAllMocks();
vi.restoreAllMocks();
});
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
@@ -167,7 +153,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
deviceId: TEST_DEVICE_ID,
...opts,
});
await initCrypto(client);
await client.initRustCrypto();
return client;
}
@@ -222,9 +208,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
);
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
@@ -248,19 +234,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// On the first decryption attempt, decryption fails.
await awaitDecryption(event);
expect(event.decryptionFailureReason).toEqual(
isNewBackend
? DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP
: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
expect(event.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
// Eventually, decryption succeeds.
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
});
it("handles error on backup query gracefully", async () => {
jest.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
@@ -272,9 +254,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
expect(calls[0].url).toEqual(EXPECTED_URL);
await flushBackupRequest();
@@ -293,11 +275,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// Send Alice a message that she won't be able to decrypt
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
expect(calls[0].url).toEqual(EXPECTED_URL);
fetchMock.resetHistory();
fetchMock.clearHistory();
// another message
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
@@ -307,12 +289,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
};
syncResponder.sendOrQueueSyncResponse(syncResponse2);
await flushBackupRequest();
expect(fetchMock.calls("getKey").length).toEqual(0);
expect(fetchMock.callHistory.calls("getKey").length).toEqual(0);
});
});
describe("recover from backup", () => {
let aliceCrypto: Crypto.CryptoApi;
let aliceCrypto: CryptoApi;
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
@@ -344,43 +326,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const check = await aliceCrypto.checkKeyBackupAndEnable();
let onKeyCached: () => void;
const awaitKeyCached = new Promise<void>((resolve) => {
onKeyCached = resolve;
});
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const result = await advanceTimersUntil(
isNewBackend
? aliceCrypto.restoreKeyBackup()
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
);
const result = await advanceTimersUntil(aliceCrypto.restoreKeyBackup());
expect(result.imported).toStrictEqual(1);
if (isNewBackend) return;
await awaitKeyCached;
// The key should be now cached
const afterCache = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!),
);
expect(afterCache.imported).toStrictEqual(1);
});
/**
@@ -412,14 +365,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
}
it("Should import full backup in chunks", async function () {
const importMockImpl = jest.fn();
if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}
const importMockImpl = vi.fn();
// @ts-ignore - mock a private method for testing purpose
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
// We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
@@ -433,20 +381,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup({
progressCallback,
})
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));
const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({
progressCallback,
});
expect(result.imported).toStrictEqual(expectedTotal);
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
@@ -480,7 +418,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
it("Should continue to process backup if a chunk import fails and report failures", async function () {
const importMockImpl = jest
const importMockImpl = vi
.fn()
.mockImplementationOnce(() => {
// Fail to import first chunk
@@ -489,13 +427,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// Ok for other chunks
.mockResolvedValue(undefined);
if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}
// @ts-ignore - mock a private method for testing purpose
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
@@ -507,18 +440,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup({ progressCallback })
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));
const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
@@ -541,13 +464,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
it("Should continue if some keys fails to decrypt", async function () {
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = jest.fn();
aliceCrypto.importBackedUpRoomKeys = vi.fn();
const decryptionFailureCount = 2;
const mockDecryptor = {
// DecryptSessions does not reject on decryption failure, but just skip the key
decryptSessions: jest.fn().mockImplementation((sessions) => {
decryptSessions: vi.fn().mockImplementation((sessions) => {
// simulate fail to decrypt 2 keys out of all
const decrypted = [];
const keys = Object.keys(sessions);
@@ -558,11 +481,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
}
return decrypted;
}),
free: jest.fn(),
free: vi.fn(),
};
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.getBackupDecryptor = jest.fn().mockResolvedValue(mockDecryptor);
aliceCrypto.getBackupDecryptor = vi.fn().mockResolvedValue(mockDecryptor);
const { response, expectedTotal } = createBackupDownloadResponse([100]);
@@ -574,100 +497,69 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
check!.backupInfo!.version!,
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup()
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
));
const result = await aliceCrypto.restoreKeyBackup();
expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
});
oldBackendOnly("recover specific session from backup", async function () {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
it("Should get the decryption key from the secret storage and restore the key backup", async function () {
// @ts-ignore - mock a private method for testing purpose
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
const fullBackup = createFullBackup(
testData.MEGOLM_SESSION_DATA.session_id,
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const check = await aliceCrypto.checkKeyBackupAndEnable();
const result = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
ROOM_ID,
testData.MEGOLM_SESSION_DATA.session_id,
check!.backupInfo!,
),
);
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);
const result = await aliceCrypto.restoreKeyBackup();
expect(result.imported).toStrictEqual(1);
});
newBackendOnly(
"Should get the decryption key from the secret storage and restore the key backup",
async function () {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
it("Should throw an error if the decryption key does not match the backup", async function () {
// Given the stored backup decryption key does not match the public backup info
// @ts-ignore - mock a private method for testing purpose
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64_ALT);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const fullBackup = createFullBackup(
testData.MEGOLM_SESSION_DATA.session_id,
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);
// When we load that key, we throw because the keys don't match
await expect(aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage()).rejects.toThrow(
DecryptionKeyDoesNotMatchError,
);
});
const result = await aliceCrypto.restoreKeyBackup();
expect(result.imported).toStrictEqual(1);
},
);
it("Should throw an error if the decryption key is not found in cache", async () => {
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
});
oldBackendOnly("Fails on bad recovery key", async function () {
const fullBackup = {
function createFullBackup(sessionId: string, data: KeyBackupSession) {
return {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
[sessionId]: data,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const check = await aliceCrypto.checkKeyBackupAndEnable();
await expect(
aliceClient.restoreKeyBackupWithRecoveryKey(
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
undefined,
undefined,
check!.backupInfo!,
),
).rejects.toThrow();
});
newBackendOnly("Should throw an error if the decryption key is not found in cache", async () => {
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
});
}
});
describe("backupLoop", () => {
it("Alice should upload known keys when backup is enabled", async function () {
// 404 means that there is no active backup
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404, { name: "room-keys-version" });
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -704,8 +596,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
fetchMock.modifyRoute("room-keys-version", {
response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -714,7 +606,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
vi.runAllTimers();
await Promise.all(uploadPromises);
@@ -738,7 +630,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
vi.runAllTimers();
await newKeyUploadPromise;
});
@@ -763,7 +655,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
name: "room-keys-version",
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -773,7 +665,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
vi.runAllTimers();
// wait for all keys to be backed up
await remainingZeroPromise;
@@ -784,10 +676,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
newBackup.version = newBackupVersion;
// Let's simulate that a new backup is available by returning error code on key upload
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
// If we import a new key the loop will try to upload to old version, it will
// fail then check the current version and switch if trusted
@@ -830,12 +719,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
vi.runAllTimers();
await disableOldBackup;
await enableNewBackup;
jest.runAllTimers();
vi.runAllTimers();
await Promise.all(uploadPromises);
await newKeyUploadPromise;
@@ -850,22 +739,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// on the first key upload attempt, simulate a network failure
const failurePromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
throw new TypeError(`Failed to fetch`);
},
{
overwriteRoutes: true,
},
);
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
resolve(undefined);
throw new TypeError(`Failed to fetch`);
});
});
// kick the import loop off and wait for the failed request
@@ -874,27 +755,21 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
jest.advanceTimersByTime(10 * 60 * 1000);
vi.advanceTimersByTime(10 * 60 * 1000);
await failurePromise;
// Fix the endpoint to do successful uploads
const successPromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
return {
status: 200,
body: {
count: 2,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
resolve(undefined);
return {
status: 200,
body: {
count: 2,
etag: "abcdefg",
},
};
});
});
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
@@ -907,7 +782,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
// run the timers, which will make the backup loop redo the request
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
await successPromise;
await allKeysUploadedPromise;
});
@@ -915,7 +790,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
it("getActiveSessionBackupVersion() should give correct result", async function () {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -934,9 +809,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// Serve a backup with no trusted signature
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
const checked = await aliceCrypto.checkKeyBackupAndEnable();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
@@ -946,9 +819,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(backupStatus).toBeNull();
// Add a valid signature to the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// check that signalling is working
const backupPromise = new Promise<void>((resolve, reject) => {
@@ -968,9 +839,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
});
newBackendOnly("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
it("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
aliceClient = await initTestClient();
@@ -986,14 +857,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
// Return now the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
expect(await aliceCrypto.getKeyBackupInfo()).toMatchObject(testData.SIGNED_BACKUP_DATA);
// Delete the backup and we are expecting the key backup to be disabled
const keyBackupStatus = defer<boolean>();
const keyBackupStatus = Promise.withResolvers<boolean>();
aliceClient.once(CryptoEvent.KeyBackupStatus, (enabled) => keyBackupStatus.resolve(enabled));
await aliceCrypto.deleteKeyBackupVersion(testData.SIGNED_BACKUP_DATA.version!);
expect(await keyBackupStatus.promise).toBe(false);
@@ -1077,7 +946,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
});
it("does not enable a backup signed by an untrusted device", async () => {
it("enables a backup not signed by a trusted device, when we have the decryption key", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// download the device list, to match the trusted-device case
await aliceClient.startClient();
await waitForDeviceList();
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// Alice does *not* trust the device that signed the backup, but *does* have the decryption key.
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: true });
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
});
it("does not enable a backup signed by an untrusted device when we do not have the decryption key", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -1102,7 +993,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
@@ -1112,9 +1003,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
delete unsignedBackup.auth_data.signatures;
unsignedBackup.version = "2";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
@@ -1129,7 +1018,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
@@ -1139,9 +1028,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
newBackup.version = newBackupVersion;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
@@ -1156,25 +1043,19 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
{
overwriteRoutes: true,
},
);
});
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
expect(noResult).toBeNull();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
@@ -1183,10 +1064,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
describe("Backup Changed from other sessions", () => {
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
name: "room-keys-version",
});
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
fetchMock.getOnce("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -1219,9 +1102,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
(url, request) => {
(callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
@@ -1235,7 +1118,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
};
}
},
{ overwriteRoutes: true },
{ name: "room-keys" },
);
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
@@ -1246,7 +1129,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
// =====
// Second suppose now that the backup has changed to version 2
@@ -1257,7 +1140,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
version: "2",
};
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { overwriteRoutes: true });
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
// suppose the new key is now known
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
@@ -1268,13 +1151,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// A check backup should happen at some point
await aliceCrypto.checkKeyBackupAndEnable();
const awaitHasQueriedNewBackup: IDeferred<void> = defer<void>();
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
(url, request) => {
fetchMock.modifyRoute("room-keys", {
response: (callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == newBackup.version) {
awaitHasQueriedNewBackup.resolve();
return testData.CURVE25519_KEY_BACKUP_DATA;
@@ -1290,8 +1172,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
};
}
},
{ overwriteRoutes: true },
);
});
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the new backup.
const newMessage: Partial<IEvent> = {
@@ -1327,7 +1208,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
-693
View File
@@ -1,693 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* This file consists of a set of integration tests which try to simulate
* communication via an Olm-encrypted room between two users, Alice and Bob.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `crypto.spec.js`.
*/
// load olm before the sdk if possible
import "../../olm-loader";
import type { Session } from "@matrix-org/olm";
import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto";
import { logger } from "../../../src/logger";
import * as testUtils from "../../test-utils/test-utils";
import { TestClient } from "../../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent, MsgType } from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { KnownMembership } from "../../../src/@types/membership";
let aliTestClient: TestClient;
const roomId = "!room:localhost";
const aliUserId = "@ali:localhost";
const aliDeviceId = "zxcvb";
const aliAccessToken = "aseukfgwef";
let bobTestClient: TestClient;
const bobUserId = "@bob:localhost";
const bobDeviceId = "bvcxz";
const bobAccessToken = "fewgfkuesa";
let aliMessages: IContent[];
let bobMessages: IContent[];
type OlmPayload = ReturnType<Session["encrypt"]>;
async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.expectDeviceKeyUpload();
await bobTestClient.httpBackend.flushAllExpected();
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
}
/**
* Set an expectation that querier will query uploader's keys; then flush the http request.
*
* @returns resolves once the http request has completed.
*/
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
// can't query keys before bob has uploaded them
expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys: Record<string, IDeviceKeys> = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result: Record<string, Record<string, IDeviceKeys>> = {};
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
return querier.httpBackend.flush("/keys/query", 1);
}
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @returns resolves once the http request has completed.
*/
async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = "";
for (keyId in keys) {
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
return { one_time_keys: result };
});
// it can take a while to process the key query, so give it some extra
// time, and make sure the claim actually happens rather than ploughing on
// confusingly.
const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500);
expect(r).toEqual(1);
}
async function aliDownloadsKeys(): Promise<void> {
// can't query keys before bob has uploaded them
expect(bobTestClient.getSigningKey()).toBeTruthy();
const p1 = async () => {
await aliTestClient.client.downloadKeys([bobUserId]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
expect(devices.length).toEqual(1);
expect(devices[0].deviceId).toEqual("bvcxz");
};
const p2 = expectAliQueryKeys;
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
await Promise.all([p1(), p2()]);
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore - protected
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
});
}
async function clientEnablesEncryption(client: MatrixClient): Promise<void> {
await client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
});
expect(client.isRoomEncrypted(roomId)).toBeTruthy();
}
const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client);
const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client);
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @returns which resolves to the ciphertext for Bob's device.
*/
async function aliSendsFirstMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client),
expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest),
]);
return ciphertext;
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @returns which resolves to the ciphertext for Bob's device.
*/
async function aliSendsMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]);
return ciphertext;
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @returns which resolves to the ciphertext for Ali's device.
*/
async function bobSendsReplyMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(bobTestClient.client),
expectBobQueryKeys().then(expectBobSendMessageRequest),
]);
return ciphertext;
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
aliMessages.push(content);
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
expect(ciphertext).toBeTruthy();
return ciphertext;
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId];
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy();
return ciphertext;
}
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage(roomId, { msgtype: MsgType.Text, body: "Hello, World" });
}
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
const path = "/send/m.room.encrypted/";
const prom = new Promise<IContent>((resolve) => {
httpBackend.when("PUT", path).respond(200, function (_path, content) {
resolve(content);
return {
event_id: "asdfgh",
};
});
});
// it can take a while to process the key query
await httpBackend.flush(path, 1);
return prom;
}
function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift()!;
return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message);
}
function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift()!;
return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message);
}
async function recvMessage(
httpBackend: TestClient["httpBackend"],
client: MatrixClient,
sender: string,
message: IContent,
): Promise<void> {
const syncData = {
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
},
},
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function (event: MatrixEvent) {
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
logger.log(client.credentials.userId + " received event", event);
client.removeListener(ClientEvent.Event, onEvent);
resolve(event);
};
client.on(ClientEvent.Event, onEvent);
});
await httpBackend.flushAllExpected();
const preDecryptionEvent = await eventPromise;
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
// it may still be being decrypted
const event = await testUtils.awaitDecryption(preDecryptionEvent);
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toMatchObject({
msgtype: "m.text",
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
}
/**
* Send an initial sync response to the client (which just includes the member
* list for our test room).
*
* @returns which resolves when the sync has been flushed.
*/
function firstSync(testClient: TestClient): Promise<void> {
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: {
[roomId]: {
state: {
events: [
testUtils.mkMembership({
mship: KnownMembership.Join,
user: aliUserId,
}),
testUtils.mkMembership({
mship: KnownMembership.Join,
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
},
},
},
};
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
return testClient.flushSync();
}
describe("MatrixClient crypto", () => {
if (!CRYPTO_ENABLED) {
return;
}
beforeEach(async () => {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initLegacyCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initLegacyCrypto();
aliMessages = [];
bobMessages = [];
});
afterEach(() => {
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", bobUploadsDeviceKeys);
it("handles failures to upload device keys", async () => {
// since device keys are uploaded asynchronously, there's not really much to do here other than fail the
// upload.
bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh"));
await bobTestClient.httpBackend.flushAllExpected();
});
it("Ali downloads Bobs device keys", async () => {
await bobUploadsDeviceKeys();
await aliDownloadsKeys();
});
it("Ali gets keys with an invalid signature", async () => {
await bobUploadsDeviceKeys();
// tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys!;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
});
it("Ali gets keys with an incorrect userId", async () => {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "bvcxz",
keys: {
"ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q",
"curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ",
},
user_id: "@eve:localhost",
signatures: {
"@eve:localhost": {
"ed25519:bvcxz":
"CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg",
},
},
};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]);
const [bobDevices, eveDevices] = await Promise.all([
aliTestClient.client.getStoredDevicesForUser(bobUserId),
aliTestClient.client.getStoredDevicesForUser(eveUserId),
]);
// should get an empty list
expect(bobDevices).toEqual([]);
expect(eveDevices).toEqual([]);
});
it("Ali gets keys with an incorrect deviceId", async () => {
const bobDeviceKeys = {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "bad_device",
keys: {
"ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0",
"curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc",
},
user_id: "@bob:localhost",
signatures: {
"@bob:localhost": {
"ed25519:bad_device":
"fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ",
},
},
};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
});
it("Bob starts his client and uploads device keys and one-time keys", async () => {
await bobTestClient.start();
const keys = await bobTestClient.awaitOneTimeKeyUpload();
expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
});
it("Ali sends a message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
});
it("Bob receives a message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
await bobRecvMessage();
});
it("Bob receives a message with a bogus sender", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
const message = aliMessages.shift()!;
const syncData = {
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
},
},
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function (event: MatrixEvent) {
logger.log(bobUserId + " received event", event);
resolve(event);
};
bobTestClient.client.once(ClientEvent.Event, onEvent);
});
await bobTestClient.httpBackend.flushAllExpected();
const preDecryptionEvent = await eventPromise;
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
// it may still be being decrypted
const event = await testUtils.awaitDecryption(preDecryptionEvent);
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
});
it("Ali blocks Bob's device", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliDownloadsKeys();
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliTestClient.client);
const p2 = expectSendMessageRequest(aliTestClient.httpBackend).then(function (sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
await Promise.all([p1, p2]);
});
it("Bob receives two pre-key messages", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
await bobRecvMessage();
await aliSendsMessage();
await bobRecvMessage();
});
it("Bob replies to the message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await firstSync(bobTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
await bobRecvMessage();
await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage();
expect(ciphertext.type).toEqual(1);
await aliRecvMessage();
});
it("Ali does a key query when encryption is enabled", async () => {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await firstSync(aliTestClient);
const syncData = {
next_batch: "2",
rooms: {
join: {
[roomId]: {
state: {
events: [
testUtils.mkEvent({
type: "m.room.encryption",
skey: "",
content: {
algorithm: "m.olm.v1.curve25519-aes-sha2",
},
}),
],
},
},
},
},
};
aliTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
await aliTestClient.httpBackend.flush("/sync", 1);
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
},
failures: {},
});
await aliTestClient.httpBackend.flushAllExpected();
});
it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => {
// Send a response which causes a key upload
const httpBackend = aliTestClient.httpBackend;
const syncDataEmpty = {
next_batch: "a",
device_one_time_keys_count: {
signed_curve25519: 0,
},
};
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
logger.log(aliTestClient + ": starting");
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
await Promise.all([aliTestClient.client.startClient({}), httpBackend.flushAllExpected()]);
logger.log(aliTestClient + ": started");
httpBackend.when("POST", "/keys/upload").respond(200, (_path, content: IUploadKeysRequest) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
});
await httpBackend.flushAllExpected();
});
it("Checks for outgoing room key requests for a given event's session", async () => {
const eventA0 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventA1 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventB = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "othersessionid",
sender_key: "senderkey",
},
});
const nonEncryptedEvent = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {},
});
aliTestClient.client.crypto?.onSyncCompleted({});
await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0);
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull();
});
});
+147 -8
View File
@@ -16,12 +16,23 @@ limitations under the License.
import Olm from "@matrix-org/olm";
import anotherjson from "another-json";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import { IContent, IDeviceKeys, IDownloadKeyResult, IEvent, Keys, MatrixClient, SigningKeys } from "../../../src";
import { IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { ISyncResponder } from "../../test-utils/SyncResponder";
import {
type IContent,
type IDeviceKeys,
type IDownloadKeyResult,
type IEvent,
type Keys,
type MatrixClient,
type SigningKeys,
} from "../../../src";
import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { type ISyncResponder } from "../../test-utils/SyncResponder";
import { syncPromise } from "../../test-utils/test-utils";
import { KeyBackupInfo } from "../../../src/crypto-api";
import { type KeyBackupInfo } from "../../../src/crypto-api";
import { logger } from "../../../src/logger";
/**
* @module
@@ -85,15 +96,15 @@ export function bootstrapCrossSigningTestOlmAccount(
deviceId: string,
keyBackupInfo: KeyBackupInfo[] = [],
): Partial<IDownloadKeyResult> {
const olmAliceMSK = new globalThis.Olm.PkSigning();
const olmAliceMSK = new Olm.PkSigning();
const masterPrivkey = olmAliceMSK.generate_seed();
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
const olmAliceUSK = new globalThis.Olm.PkSigning();
const olmAliceUSK = new Olm.PkSigning();
const userPrivkey = olmAliceUSK.generate_seed();
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
const olmAliceSSK = new globalThis.Olm.PkSigning();
const olmAliceSSK = new Olm.PkSigning();
const sskPrivkey = olmAliceSSK.generate_seed();
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
@@ -181,7 +192,7 @@ export async function createOlmSession(
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new globalThis.Olm.Session();
const session = new Olm.Session();
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
}
@@ -294,6 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
},
type: "m.room.encrypted",
unsigned: {},
state_key: opts.plaintext.hasOwnProperty("state_key")
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
: undefined,
};
}
@@ -406,3 +420,128 @@ export async function establishOlmSession(
await syncPromise(testClient);
return p2pSession;
}
/**
* Expect that the client shares keys with the given recipient
*
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
* to establish an Olm InboundGroupSession.
*
* @param recipientUserID - the user id of the expected recipient
*
* @param recipientOlmAccount - Olm.Account for the recipient
*
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
*
* @returns the established inbound group session
*/
export async function expectSendRoomKey(
recipientUserID: string,
recipientOlmAccount: Olm.Account,
recipientOlmSession: Olm.Session | null = null,
): Promise<Olm.InboundGroupSession> {
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
function onSendRoomKey(content: any): Olm.InboundGroupSession {
const m = content.messages[recipientUserID].DEVICE_ID;
const ct = m.ciphertext[testRecipientKey];
if (!recipientOlmSession) {
expect(ct.type).toEqual(0); // pre-key message
recipientOlmSession = new Olm.Session();
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
} else {
expect(ct.type).toEqual(1); // regular message
}
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual("m.room_key");
const inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return inboundGroupSession;
}
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
const content = JSON.parse(callLog.options.body as string);
resolve(onSendRoomKey(content));
return {};
});
});
}
/**
* Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint.
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
* @returns the content of the encrypted event
*/
export function expectEncryptedSendMessageEvent() {
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return { event_id: "$event_id" };
});
});
}
/**
* Return the event received on rooms/{roomId}/state/m.room.encrypted/{stateKey} endpoint.
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
* @returns the content of the encrypted event
*/
function expectEncryptedSendStateEvent() {
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(new RegExp("/state/m.room.encrypted/"), (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return { event_id: "$event_id" };
});
});
}
/**
* Expect that the client sends an encrypted message event
*
* Waits for an HTTP request to send an encrypted message in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted event
*/
export async function expectSendMegolmMessageEvent(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedMessageContent = await expectEncryptedSendMessageEvent();
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
const inboundGroupSession = await inboundGroupSessionPromise;
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
logger.log("Decrypted received megolm message", r);
return JSON.parse(r.plaintext);
}
/**
* Expect that the client sends an encrypted state event
*
* Waits for an HTTP request to send an encrypted state event in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted state event
*/
export async function expectSendMegolmStateEvent(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedStateContent = await expectEncryptedSendStateEvent();
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
const inboundGroupSession = await inboundGroupSessionPromise;
const r: any = inboundGroupSession.decrypt(encryptedStateContent!.ciphertext);
logger.log("Decrypted received megolm state event", r);
return JSON.parse(r.plaintext);
}
+43 -9
View File
@@ -16,16 +16,17 @@ limitations under the License.
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { createClient, CryptoEvent, IndexedDBCryptoStore } from "../../../src";
import { createClient, IndexedDBCryptoStore } from "../../../src";
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
import { CryptoEvent } from "../../../src/crypto-api";
jest.setTimeout(15000);
vi.setConfig({ testTimeout: 15000 });
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -66,6 +67,23 @@ describe("MatrixClient.initRustCrypto", () => {
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
});
it("should create the indexed db with a custom prefix", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto({ cryptoDatabasePrefix: "my-prefix" });
// should have an indexed db now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(expect.arrayContaining(["my-prefix::matrix-sdk-crypto"]));
});
it("should create the meta db if given a storageKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -104,6 +122,7 @@ describe("MatrixClient.initRustCrypto", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should ignore a second call", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -116,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
});
describe("Libolm Migration", () => {
beforeEach(() => {
fetchMock.reset();
});
it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
@@ -137,7 +152,7 @@ describe("MatrixClient.initRustCrypto", () => {
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
});
const progressListener = jest.fn();
const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
@@ -308,7 +323,7 @@ describe("MatrixClient.initRustCrypto", () => {
});
// When we start Rust crypto, potentially triggering an upgrade
const progressListener = jest.fn();
const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
@@ -460,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
expect(await indexedDB.databases()).toHaveLength(0);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not fail in environments without indexedDB", async () => {
// eslint-disable-next-line no-global-assign
indexedDB = undefined!;
@@ -474,4 +490,22 @@ describe("MatrixClient.clearStores", () => {
await matrixClient.clearStores();
// No error thrown in clearStores
});
it("should clear the indexeddbs with a custom prefix", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto({ cryptoDatabasePrefix: "my-prefix" });
expect(await indexedDB.databases()).toHaveLength(1);
await matrixClient.stopClient();
await matrixClient.clearStores({ cryptoDatabasePrefix: "my-prefix" });
expect(await indexedDB.databases()).toHaveLength(0);
});
});
+221
View File
@@ -0,0 +1,221 @@
/*
Copyright 2025 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import anotherjson from "another-json";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import Olm from "@matrix-org/olm";
import * as testUtils from "../../test-utils/test-utils";
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
import { TEST_ROOM_ID as ROOM_ID } from "../../test-utils/test-data";
import { logger } from "../../../src/logger";
import {
createClient,
HistoryVisibility,
PendingEventOrdering,
type IStartClientOpts,
type MatrixClient,
} from "../../../src/matrix";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import {
createOlmAccount,
createOlmSession,
encryptGroupSessionKey,
encryptMegolmEvent,
getTestOlmAccountKeys,
expectSendRoomKey,
expectSendMegolmStateEvent,
} from "./olm-utils";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
describe("Encrypted State Events", () => {
let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = "";
/** the MatrixClient under test */
let aliceClient: MatrixClient;
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
let keyReceiver: E2EKeyReceiver;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
logger.log(aliceClient.getUserId() + ": starting");
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
aliceClient.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: PendingEventOrdering.Detached,
...opts,
});
await syncPromise(aliceClient);
logger.log(aliceClient.getUserId() + ": started");
}
beforeEach(async () => {
fetchMock.catch(404);
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: "@alice:localhost",
accessToken: "akjgkrgjs",
deviceId: "xzcvb",
logger: logger.getChild("aliceClient"),
enableEncryptedStateEvents: true,
});
keyReceiver = new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
await aliceClient.initRustCrypto();
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
testOlmAccount = await createOlmAccount();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
}, 10000);
afterEach(async () => {
aliceClient.stopClient();
});
function expectAliceKeyQuery(response: any) {
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
}
function expectAliceKeyClaim(response: any) {
fetchMock.postOnce(new RegExp("/keys/claim"), response);
}
function getTestKeysClaimResponse(userId: string) {
testOlmAccount.generate_one_time_keys(1);
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
testOlmAccount.mark_keys_as_published();
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
const oneTimeKey: string = testOneTimeKeys.curve25519[keyId];
const unsignedKeyResult = { key: oneTimeKey };
const j = anotherjson.stringify(unsignedKeyResult);
const sig = testOlmAccount.sign(j);
const keyResult = {
...unsignedKeyResult,
signatures: { [userId]: { "ed25519:DEVICE_ID": sig } },
};
return {
one_time_keys: { [userId]: { DEVICE_ID: { ["signed_curve25519:" + keyId]: keyResult } } },
failures: {},
};
}
it("Should receive an encrypted state event", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a state event with the group session
const eventEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
plaintext: {
type: "m.room.topic",
state_key: "",
content: {
topic: "Secret!",
},
},
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [eventEncrypted] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(decryptedEvent.getContent().topic).toEqual("Secret!");
});
// eslint-disable-next-line @vitest/expect-expect
it("Should send an encrypted state event", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
keyResponder.addDeviceKeys(testDeviceKeys);
await startClientAndAwaitFirstSync();
// Alice shares a room with Bob
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
await syncPromise(aliceClient);
// ... and claim one of Bob's OTKs ...
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
// ... and send an m.room.topic message
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
await Promise.all([
aliceClient.setRoomTopic(ROOM_ID, "Secret!"),
expectSendMegolmStateEvent(inboundGroupSessionPromise),
]);
});
});
+125 -9
View File
@@ -14,16 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm";
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { createClient, MatrixClient } from "../../../src";
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
import {
ClientEvent,
createClient,
type IToDeviceEvent,
type MatrixClient,
type MatrixEvent,
type ReceivedToDeviceMessage,
} from "../../../src";
import * as testData from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -38,17 +47,18 @@ afterEach(() => {
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backend: string, initCrypto: InitCrypto) => {
describe("to-device-messages", () => {
let aliceClient: MatrixClient;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
let e2eKeyReceiver: E2EKeyReceiver;
let syncResponder: SyncResponder;
beforeEach(
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://server.com";
aliceClient = createClient({
@@ -59,8 +69,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe
});
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
new E2EKeyReceiver(homeserverUrl);
const syncResponder = new SyncResponder(homeserverUrl);
e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
// add bob as known user
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
@@ -81,7 +91,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe
{ filter_id: "fid" },
);
await initCrypto(aliceClient);
await aliceClient.initRustCrypto();
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
@@ -89,7 +99,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe
afterEach(async () => {
aliceClient.stopClient();
fetchMock.mockReset();
});
describe("encryptToDeviceMessages", () => {
@@ -149,4 +158,111 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backe
// for future: check that bob's device can decrypt the ciphertext?
});
});
describe("receive to-device-messages", () => {
it("Should receive decrypted to-device message via ClientEvent", async () => {
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
await Olm.init();
const testOlmAccount = new Olm.Account();
testOlmAccount.create();
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
e2eKeyResponder.addDeviceKeys(testDeviceKeys);
await aliceClient.startClient();
await syncPromise(aliceClient);
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
await syncPromise(aliceClient);
const p2pSession = await establishOlmSession(aliceClient, e2eKeyReceiver, syncResponder, testOlmAccount);
const toDeviceEvent = encryptOlmEvent({
sender: "@bob:xyz",
senderKey: testDeviceKeys.keys[`curve25519:DEVICE_ID`],
senderSigningKey: testDeviceKeys.keys[`ed25519:DEVICE_ID`],
p2pSession: p2pSession,
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
plaincontent: {
body: "foo",
},
plaintype: "m.test.type",
});
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
processedToDeviceResolver.resolve(payload);
});
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
oldToDeviceResolver.resolve(event);
});
expect(toDeviceEvent.type).toBe("m.room.encrypted");
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
await syncPromise(aliceClient);
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
expect(message.type).toBe("m.test.type");
expect(message.content["body"]).toBe("foo");
expect(encryptionInfo).not.toBeNull();
expect(encryptionInfo!.senderVerified).toBe(false);
expect(encryptionInfo!.sender).toBe("@bob:xyz");
expect(encryptionInfo!.senderDevice).toBe("DEVICE_ID");
const oldFormat = await oldToDeviceResolver.promise;
expect(oldFormat.isEncrypted()).toBe(true);
expect(oldFormat.getType()).toBe("m.test.type");
expect(oldFormat.getContent()["body"]).toBe("foo");
});
it("Should receive clear to-device message via ClientEvent", async () => {
await aliceClient.startClient();
await syncPromise(aliceClient);
const toDeviceEvent: IToDeviceEvent = {
sender: "@bob:xyz",
type: "m.test.type",
content: {
body: "foo",
},
};
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
processedToDeviceResolver.resolve(payload);
});
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
oldToDeviceResolver.resolve(event);
});
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
await syncPromise(aliceClient);
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
expect(message.type).toBe("m.test.type");
expect(message.content["body"]).toBe("foo");
// When the message is not encrypted, we don't have the encryptionInfo.
expect(encryptionInfo).toBeNull();
const oldFormat = await oldToDeviceResolver.promise;
expect(oldFormat.isEncrypted()).toBe(false);
expect(oldFormat.getType()).toBe("m.test.type");
expect(oldFormat.getContent()["body"]).toBe("foo");
});
});
});
+268 -220
View File
@@ -17,42 +17,37 @@ limitations under the License.
import "fake-indexeddb/auto";
import anotherjson from "another-json";
import FetchMock from "fetch-mock";
import fetchMock from "fetch-mock-jest";
import debug from "debug";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
import Olm from "@matrix-org/olm";
import {
createClient,
CryptoEvent,
DebugLogger,
DeviceVerification,
IContent,
ICreateClientOpts,
IEvent,
MatrixClient,
type IContent,
type ICreateClientOpts,
type IEvent,
type MatrixClient,
MatrixError,
MatrixEvent,
MatrixEventEvent,
} from "../../../src";
import {
canAcceptVerificationRequest,
ShowQrCodeCallbacks,
ShowSasCallbacks,
type ShowQrCodeCallbacks,
type ShowSasCallbacks,
VerificationPhase,
VerificationRequest,
type VerificationRequest,
VerificationRequestEvent,
Verifier,
type Verifier,
VerifierEvent,
} from "../../../src/crypto-api/verification";
import { defer, escapeRegExp } from "../../../src/utils";
import {
awaitDecryption,
CRYPTO_BACKENDS,
emitPromise,
getSyncResponse,
InitCrypto,
syncPromise,
} from "../../test-utils/test-utils";
import { escapeRegExp, sleep } from "../../../src/utils";
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise, waitFor } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import {
BACKUP_DECRYPTION_KEY_BASE64,
@@ -79,19 +74,14 @@ import {
encryptMegolmEvent,
encryptSecretSend,
getTestOlmAccountKeys,
ToDeviceEvent,
type ToDeviceEvent,
} from "./olm-utils";
import { KeyBackupInfo } from "../../../src/crypto-api";
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
import { encodeBase64 } from "../../../src/base64";
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
beforeAll(async () => {
// we use the libolm primitives in the test, so init the Olm library
await globalThis.Olm.init();
await Olm.init();
});
// load the rust library. This can take a few seconds on a slow GH worker.
@@ -101,6 +91,10 @@ beforeAll(async () => {
await RustSdkCryptoJs.initAsync();
}, 10000);
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
@@ -117,12 +111,7 @@ const TEST_HOMESERVER_URL = "https://alice-server.com";
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
// we test with both crypto stacks...
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => {
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
describe("verification", () => {
/** the client under test */
let aliceClient: MatrixClient;
@@ -138,7 +127,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
@@ -149,14 +137,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
aliceClient?.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
if (vi.isFakeTimers()) {
await vi.runAllTimersAsync();
}
});
describe("Outgoing verification requests for another device", () => {
@@ -164,11 +150,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// pretend that we have another device, which we will verify
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
{ ok: false, status: 404 },
{ overwriteRoutes: true },
);
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
ok: false,
status: 404,
});
});
// test with (1) the default verification method list, (2) a custom verification method list.
@@ -220,7 +205,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
if (methods !== undefined) {
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
}
@@ -253,7 +238,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
vi.advanceTimersByTime(10);
requestBody = await sendToDevicePromise;
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
@@ -265,7 +250,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
const olmSAS = new globalThis.Olm.SAS();
const olmSAS = new Olm.SAS();
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
// alice responds with a 'key' ...
@@ -331,7 +316,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// And now Alice starts a SAS verification
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -359,7 +344,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
const olmSAS = new globalThis.Olm.SAS();
const olmSAS = new Olm.SAS();
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
@@ -432,9 +417,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(requests[0].transactionId).toEqual(transactionId);
}
// legacy crypto picks devices individually; rust crypto uses a broadcast message
const toDeviceMessage =
requestBody.messages[TEST_USER_ID]["*"] ?? requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
// rust crypto uses a broadcast message
const toDeviceMessage = requestBody.messages[TEST_USER_ID]["*"];
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
});
@@ -522,18 +506,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
reciprocateQRCodeCallbacks.confirm();
await sendToDevicePromise;
// at this point, on legacy crypto, the master key is already marked as trusted, and the request is "Done".
// Rust crypto, on the other hand, waits for the 'done' to arrive from the other side.
// Rust crypto waits for the 'done' to arrive from the other side.
if (request.phase === VerificationPhase.Done) {
// legacy crypto: we're all done
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
await verificationPromise;
} else {
// rust crypto: still in flight
// eslint-disable-next-line jest/no-conditional-expect
expect(request.phase).toEqual(VerificationPhase.Started);
}
// the dummy device replies with its own 'done'
@@ -569,7 +547,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(qrCodeBuffer).toBeUndefined();
});
newBackendOnly("can verify another by scanning their QR code", async () => {
it("can verify another by scanning their QR code", async () => {
aliceClient = await startTestClient();
// we need cross-signing keys for a QR code verification
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
@@ -654,7 +632,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// ... but Alice wants to do an SAS verification
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -699,7 +677,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// ... but the dummy device wants to do an SAS verification
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
@@ -752,6 +730,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.cancellingUserId).toEqual("@alice:localhost");
});
it("does not include cancelled requests in the list of requests", async () => {
// Given Alice started a verification request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId!;
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
await waitForVerificationRequestChanged(request);
// Sanity: the request is listed
const requestsBeforeCancel = aliceClient
.getCrypto()!
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
expect(requestsBeforeCancel).toHaveLength(1);
// When Alice cancels it
await Promise.all([expectSendToDeviceMessage("m.key.verification.cancel"), request.cancel()]);
// Then it is no longer listed as in progress
const requestsAfterCancel = aliceClient
.getCrypto()!
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
expect(requestsAfterCancel).toHaveLength(0);
});
it("can cancel during the SAS phase", async () => {
// have alice initiate a verification. She should send a m.key.verification.request
const [, request] = await Promise.all([
@@ -778,7 +785,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
vi.advanceTimersByTime(10);
await sendToDevicePromise;
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
@@ -907,7 +914,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
describe("Send verification request in DM", () => {
beforeEach(async () => {
aliceClient = await startTestClient();
aliceClient.setGlobalErrorOnUnknownDevices(false);
e2eKeyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
e2eKeyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
@@ -924,19 +930,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
function awaitRoomMessageRequest(): Promise<IContent> {
return new Promise((resolve) => {
// Case of unencrypted message of the new crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
(url: string, options: RequestInit) => {
resolve(JSON.parse(options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
},
);
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
resolve(JSON.parse(callLog.options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
});
// Case of encrypted message of the old crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
async (url: string, options: RequestInit) => {
const encryptedMessage = JSON.parse(options.body as string);
async (callLog) => {
const encryptedMessage = JSON.parse(callLog.options.body as string);
const event = new MatrixEvent({
content: encryptedMessage,
type: "m.room.encrypted",
@@ -959,7 +962,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
// 5ms by users, ie Bob and Alice
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
const messageRequestPromise = awaitRoomMessageRequest();
const verificationRequest = await aliceClient
@@ -990,21 +993,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
testOlmAccount.create();
aliceClient = await startTestClient();
aliceClient.setGlobalErrorOnUnknownDevices(false);
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
await syncPromise(aliceClient);
// Rust crypto requires the sender's device keys before it accepts a
// verification request.
if (backend === "rust-sdk") {
const crypto = aliceClient.getCrypto()!;
const crypto = aliceClient.getCrypto()!;
const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice");
e2eKeyResponder.addDeviceKeys(bobDeviceKeys);
syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } });
await syncPromise(aliceClient);
await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]);
}
const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice");
e2eKeyResponder.addDeviceKeys(bobDeviceKeys);
syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } });
await syncPromise(aliceClient);
await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]);
});
/**
@@ -1072,7 +1072,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
it("ignores old verification requests", async () => {
const eventHandler = jest.fn();
const debug = vi.fn();
const info = vi.fn();
const warn = vi.fn();
// @ts-ignore overriding RustCrypto's logger
aliceClient.getCrypto()!.logger = { debug, info, warn };
const eventHandler = vi.fn();
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
const verificationRequestEvent = createVerificationRequestEvent();
@@ -1086,6 +1093,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
const matrixEvent = room.getLiveTimeline().getEvents()[0];
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
// Wait until the request has been processed. We use a real sleep()
// here to make sure any background async tasks are completed.
vi.useRealTimers();
await waitFor(async () => {
expect(info).toHaveBeenCalledWith(
expect.stringMatching(/^Ignoring just-received verification request/),
);
sleep(100);
});
// check that an event has not been raised, and that the request is not found
expect(eventHandler).not.toHaveBeenCalled();
expect(
@@ -1093,6 +1110,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
).not.toBeDefined();
});
it("ignores cancelled verification requests", async () => {
// Given a verification request exists
const event = createVerificationRequestEvent();
returnRoomMessageFromSync(TEST_ROOM_ID, event);
// Wait for the request to be received
await emitPromise(aliceClient, CryptoEvent.VerificationRequestReceived);
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// When I cancel it
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.key.verification.cancel/:id", {
event_id: event.event_id,
});
await request!.cancel();
expect(request!.phase).toEqual(VerificationPhase.Cancelled);
// Then it is no longer found
expect(
aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz"),
).not.toBeDefined();
});
it("Plaintext verification request from Bob to Alice", async () => {
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
@@ -1137,7 +1177,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
returnToDeviceMessageFromSync(toDeviceEvent);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// Wait for the request to be decrypted
const request1 = await requestEventPromise;
@@ -1152,43 +1192,40 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request?.otherUserId).toBe("@bob:xyz");
});
newBackendOnly(
"If the verification request is not decrypted within 5 minutes, the request is ignored",
async () => {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
it("If the verification request is not decrypted within 5 minutes, the request is ignored", async () => {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
// make the room_key event, but don't send it yet
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
// Wait for the sync response to be processed
await syncPromise(aliceClient);
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
// wait for a first attempt at decryption: should fail
await awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// wait for a first attempt at decryption: should fail
await awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Advance time by 5mins, the verification request should be ignored after that
jest.advanceTimersByTime(5 * 60 * 1000);
// Advance time by 5mins, the verification request should be ignored after that
vi.advanceTimersByTime(5 * 60 * 1000);
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
// Wait for the message to be decrypted
await awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
// Wait for the message to be decrypted
await awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// the request should not be present
expect(request).not.toBeDefined();
},
);
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// the request should not be present
expect(request).not.toBeDefined();
});
});
describe("Secrets are gossiped after verification", () => {
@@ -1243,7 +1280,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
await syncPromise(aliceClient);
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the olm device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1255,12 +1292,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
testOlmAccount?.free();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
await vi.runAllTimersAsync();
});
newBackendOnly("Should request cross signing keys after verification", async () => {
// eslint-disable-next-line @vitest/expect-expect
it("Should request cross signing keys after verification", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
@@ -1271,7 +1307,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
await requestPromises.get("m.cross_signing.self_signing");
});
newBackendOnly("Should accept the backup decryption key gossip if valid", async () => {
it("Should accept the backup decryption key gossip if valid", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
@@ -1290,7 +1326,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(encodeBase64(cachedKey!)).toEqual(BACKUP_DECRYPTION_KEY_BASE64);
});
newBackendOnly("Should not accept the backup decryption key gossip if private key do not match", async () => {
it("Should not accept the backup decryption key gossip when there is no server-side key backup", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
await sendBackupGossipAndExpectVersion(
requestId!,
BACKUP_DECRYPTION_KEY_BASE64,
new MatrixError({ errcode: "M_NOT_FOUND", error: "No backup found" }, 404),
);
// the backup secret should not be cached
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
expect(cachedKey).toBeNull();
});
it("Should not accept the backup decryption key gossip when server-side key backup request errors", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
await sendBackupGossipAndExpectVersion(
requestId!,
BACKUP_DECRYPTION_KEY_BASE64,
new Error("Network Error!"),
);
// the backup secret should not be cached
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
expect(cachedKey).toBeNull();
});
it("Should not accept the backup decryption key gossip if private key do not match", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
@@ -1299,43 +1371,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, nonMatchingBackupInfo);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
expect(cachedKey).toBeNull();
});
newBackendOnly("Should not accept the backup decryption key gossip if backup not trusted", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
const infoCopy = Object.assign({}, matchingBackupInfo);
delete infoCopy.auth_data.signatures;
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, infoCopy);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
expect(cachedKey).toBeNull();
});
newBackendOnly("Should not accept the backup decryption key gossip if backup algorithm unknown", async () => {
it("Should not accept the backup decryption key gossip if backup algorithm unknown", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
@@ -1348,19 +1389,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
unknownAlgorithmBackupInfo,
);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
expect(cachedKey).toBeNull();
});
newBackendOnly("Should not accept an invalid backup decryption key", async () => {
it("Should not accept an invalid backup decryption key", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
@@ -1369,26 +1403,38 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
await sendBackupGossipAndExpectVersion(requestId!, "InvalidSecret", matchingBackupInfo);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
expect(cachedKey).toBeNull();
});
/**
* Waits briefly for secrets to be gossipped, then fetches the backup private key from the crypto stack.
*/
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
// We are lacking a way to signal that the secret has been received, so we wait a bit..
vi.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
vi.useFakeTimers();
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
}
/**
* Common test setup for gossiping secrets.
* Creates a peer to peer session, sends the secret, mockup the version API, send the secret back from sync, then await for the backup check.
*
* @param expectBackup - The result to be returned from the `/room_keys/version` request.
* - **KeyBackupInfo**: Indicates a successful request, where the response contains the key backup information (HTTP 200).
* - **MatrixError**: Represents an error response from the server, indicating an unsuccessful request (non-200 HTTP status).
* - **Error**: Indicates an error during the request process itself (e.g., network issues or unexpected failures).
*/
async function sendBackupGossipAndExpectVersion(
requestId: string,
secret: string,
expectBackup: KeyBackupInfo,
expectBackup: KeyBackupInfo | MatrixError | Error,
) {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
@@ -1404,16 +1450,21 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
const expectBackupCheck = new Promise((resolve) => {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/version",
(url, request) => {
resolve(undefined);
return expectBackup;
},
{
overwriteRoutes: true,
},
);
fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
resolve(undefined);
if (expectBackup instanceof MatrixError) {
return {
status: expectBackup.httpStatus,
body: expectBackup.data,
};
}
if (expectBackup instanceof Error) {
return Promise.reject(expectBackup);
}
return expectBackup;
});
});
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
@@ -1480,9 +1531,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: "device_under_test",
logger: new DebugLogger(debug(`matrix-js-sdk:verification`)),
...opts,
});
await initCrypto(client);
await client.initRustCrypto();
await client.startClient();
return client;
}
@@ -1493,7 +1545,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1527,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
return new Promise((resolve) => {
fetchMock.putOnce(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
resolve(JSON.parse(opts.body as string));
(callLog): RouteResponse => {
resolve(JSON.parse(callLog.options.body as string));
return {};
},
);
@@ -1544,40 +1596,36 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
* @returns a map of secret name to promise that will resolve (with the id of the secret request) when the secret is requested.
*/
function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
const mskRequestDefer = defer<string>();
const sskRequestDefer = defer<string>();
const uskRequestDefer = defer<string>();
const backupKeyRequestDefer = defer<string>();
const mskRequestResolvers = Promise.withResolvers<string>();
const sskRequestResolvers = Promise.withResolvers<string>();
const uskRequestResolvers = Promise.withResolvers<string>();
const backupKeyRequestResolvers = Promise.withResolvers<string>();
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
if (content.action == "request") {
const name = content.name;
const requestId = content.request_id;
if (name == "m.cross_signing.user_signing") {
uskRequestDefer.resolve(requestId);
} else if (name == "m.cross_signing.master") {
mskRequestDefer.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") {
sskRequestDefer.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestDefer.resolve(requestId);
}
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
if (content.action == "request") {
const name = content.name;
const requestId = content.request_id;
if (name == "m.cross_signing.user_signing") {
uskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.master") {
mskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") {
sskRequestResolvers.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestResolvers.resolve(requestId);
}
return {};
},
{ overwriteRoutes: true },
);
}
return {};
});
const promiseMap = new Map<string, Promise<string>>();
promiseMap.set("m.cross_signing.master", mskRequestDefer.promise);
promiseMap.set("m.cross_signing.self_signing", sskRequestDefer.promise);
promiseMap.set("m.cross_signing.user_signing", uskRequestDefer.promise);
promiseMap.set("m.megolm_backup.v1", backupKeyRequestDefer.promise);
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
promiseMap.set("m.cross_signing.self_signing", sskRequestResolvers.promise);
promiseMap.set("m.cross_signing.user_signing", uskRequestResolvers.promise);
promiseMap.set("m.megolm_backup.v1", backupKeyRequestResolvers.promise);
return promiseMap;
}
@@ -1608,7 +1656,7 @@ function sha256(commitmentStr: string): string {
return encodeUnpaddedBase64(createHash("sha256").update(commitmentStr, "utf8").digest());
}
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
function encodeUnpaddedBase64(uint8Array: ArrayLike<number>): string {
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
}
@@ -1642,7 +1690,7 @@ function buildReadyMessage(
}
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) {
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayLike<number>) {
return {
type: "m.key.verification.start",
content: {
-406
View File
@@ -1,406 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TestClient } from "../TestClient";
import * as testUtils from "../test-utils/test-utils";
import { logger } from "../../src/logger";
import { KnownMembership } from "../../src/@types/membership";
const ROOM_ID = "!room:id";
/**
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @returns sync response
*/
function getSyncResponse(roomMembers: string[]) {
const stateEvents = [
testUtils.mkEvent({
type: "m.room.encryption",
skey: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
}),
];
Array.prototype.push.apply(
stateEvents,
roomMembers.map((m) =>
testUtils.mkMembership({
mship: KnownMembership.Join,
sender: m,
}),
),
);
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
state: {
events: stateEvents,
},
},
},
},
};
return syncResponse;
}
describe("DeviceList management:", function () {
if (!globalThis.Olm) {
logger.warn("not running deviceList tests: Olm not present");
return;
}
let aliceTestClient: TestClient;
let sessionStoreBackend: Storage;
async function createTestClient() {
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
await testClient.client.initLegacyCrypto();
return testClient;
}
beforeEach(async function () {
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi();
aliceTestClient = await createTestClient();
});
afterEach(function () {
return aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function () {
aliceTestClient.expectKeyQuery({
device_keys: { "@alice:localhost": {} },
failures: {},
});
return aliceTestClient
.start()
.then(function () {
const syncResponse = getSyncResponse(["@bob:xyz"]);
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
})
.then(function () {
logger.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(["@bob:xyz"]),
aliceTestClient.httpBackend.flush("/keys/query", 1),
]);
})
.then(function () {
logger.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, {
event_id: "$event_id",
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it.skip("We should not get confused by out-of-order device query responses", () => {
// https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({
device_keys: { "@alice:localhost": {} },
failures: {},
});
return aliceTestClient
.start()
.then(() => {
aliceTestClient.httpBackend
.when("GET", "/sync")
.respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"]));
return aliceTestClient.flushSync();
})
.then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
"@chris:abc": {},
},
});
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" });
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
aliceTestClient.httpBackend
.flush("/keys/query", 1)
.then(() => aliceTestClient.httpBackend.flush("/send/", 1)),
aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
]);
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
expect(data!.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: "2",
device_lists: {
changed: ["@bob:xyz"],
},
});
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: "3",
device_lists: {
changed: ["@chris:abc"],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
})
.then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend
.when("POST", "/keys/query", {
device_keys: {
"@chris:abc": {},
},
token: "3",
})
.respond(200, {
device_keys: { "@chris:abc": {} },
});
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
if (bobStat != 1 && bobStat != 2) {
throw new Error("Unexpected status for bob: wanted 1 or 2, got " + bobStat);
}
const chrisStat = data!.trackingStatus["@chris:abc"];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + chrisStat);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend
.when("POST", "/keys/query", {
device_keys: {
"@bob:xyz": {},
},
token: "2",
})
.respond(200, {
device_keys: { "@bob:xyz": {} },
});
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(["@bob:xyz"]);
})
.then(() => {
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
expect(bobStat).toEqual(3);
const chrisStat = data!.trackingStatus["@chris:abc"];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + bobStat);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(["@chris:abc"]);
})
.then(() => {
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
const chrisStat = data!.trackingStatus["@bob:xyz"];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data!.syncToken).toEqual(3);
});
});
});
// https://github.com/vector-im/element-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function () {
await aliceTestClient.start();
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
},
});
await aliceTestClient.httpBackend.flush("/keys/query", 1);
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan(0);
});
});
it("when Bob leaves", async function () {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
device_lists: {
left: ["@bob:xyz"],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: KnownMembership.Leave,
sender: "@bob:xyz",
}),
],
},
},
},
},
});
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(0);
});
});
it("when Alice leaves", async function () {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
device_lists: {
left: ["@bob:xyz"],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: KnownMembership.Leave,
sender: "@bob:xyz",
}),
],
},
},
},
},
});
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(0);
});
});
it("when Bob leaves whilst Alice is offline", async function () {
aliceTestClient.stop();
const anotherTestClient = await createTestClient();
try {
await anotherTestClient.start();
anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
// @ts-ignore accessing private property
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(0);
});
} finally {
anotherTestClient.stop();
}
});
});
});
@@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import type HttpBackend from "matrix-mock-request";
import {
ClientEvent,
HttpApiEvent,
IEvent,
MatrixClient,
type IEvent,
type MatrixClient,
RoomEvent,
RoomMemberEvent,
RoomStateEvent,
@@ -23,15 +23,15 @@ import {
EventTimelineSet,
EventType,
Filter,
IEvent,
MatrixClient,
type IEvent,
type MatrixClient,
MatrixEvent,
PendingEventOrdering,
RelationType,
Room,
} from "../../src/matrix";
import { logger } from "../../src/logger";
import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/utils";
import { encodeParams, encodeUri, type QueryDict, replaceParam } from "../../src/utils";
import { TestClient } from "../TestClient";
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
import { emitPromise } from "../test-utils/test-utils";
@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
});
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
it("should return null when event is not in the thread that the given timelineSet is representing", () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
});
return Promise.all([
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(),
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(),
]);
});
it("should return undefined when event is within a thread but timelineSet is not", () => {
it("should return null when event is within a thread but timelineSet is not", () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
});
return Promise.all([
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(),
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(),
]);
});
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
}
// eslint-disable-next-line @vitest/expect-expect
it("in stable mode", async () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
+115 -193
View File
@@ -13,28 +13,26 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import { Mocked } from "jest-mock";
import type HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
import { type IStoredClientOpts, MatrixClient } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import {
Filter,
JoinRule,
KnockRoomOpts,
type KnockRoomOpts,
MemoryStore,
Method,
Room,
RoomSummary,
type RoomSummary,
SERVICE_TYPES,
} from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IFilterDefinition } from "../../src/filter";
import { ISearchResults } from "../../src/@types/search";
import { IStore } from "../../src/store";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { type IFilterDefinition } from "../../src/filter";
import { type ISearchResults } from "../../src/@types/search";
import { type IStore } from "../../src/store";
import { SetPresence } from "../../src/sync";
import { KnownMembership } from "../../src/@types/membership";
@@ -159,6 +157,30 @@ describe("MatrixClient", function () {
});
});
describe("mediaConfig", function () {
it("should get media config on unauthenticated media call", async () => {
httpBackend.when("GET", "/_matrix/media/v3/config").respond(200, '{"m.upload.size": 50000000}', true);
const prom = client.getMediaConfig();
httpBackend.flushAllExpected();
expect((await prom)["m.upload.size"]).toEqual(50000000);
});
it("should get media config on authenticated media call", async () => {
httpBackend
.when("GET", "/_matrix/client/v1/media/config")
.respond(200, '{"m.upload.size": 50000000}', true);
const prom = client.getMediaConfig(true);
httpBackend.flushAllExpected();
expect((await prom)["m.upload.size"]).toEqual(50000000);
});
});
describe("joinRoom", function () {
it("should no-op given the ID of a room you've already joined", async () => {
const roomId = "!foo:bar";
@@ -245,6 +267,59 @@ describe("MatrixClient", function () {
});
});
describe("invite", function () {
it("should send request to /invite", async () => {
const roomId = "!roomId:server";
const userId = "@user:server";
httpBackend
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
.check((request) => {
expect(request.data).toEqual({ user_id: userId });
})
.respond(200, {});
const prom = client.invite(roomId, userId);
await httpBackend.flushAllExpected();
await prom;
httpBackend.verifyNoOutstandingExpectation();
});
it("accepts a stringy reason argument", async () => {
const roomId = "!roomId:server";
const userId = "@user:server";
httpBackend
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
.check((request) => {
expect(request.data).toEqual({ user_id: userId, reason: "testreason" });
})
.respond(200, {});
const prom = client.invite(roomId, userId, "testreason");
await httpBackend.flushAllExpected();
await prom;
httpBackend.verifyNoOutstandingExpectation();
});
it("accepts an options object with a reason", async () => {
const roomId = "!roomId:server";
const userId = "@user:server";
httpBackend
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
.check((request) => {
expect(request.data).toEqual({ user_id: userId, reason: "testreason" });
})
.respond(200, {});
const prom = client.invite(roomId, userId, { reason: "testreason" });
await httpBackend.flushAllExpected();
await prom;
httpBackend.verifyNoOutstandingExpectation();
});
});
describe("knockRoom", function () {
const roomId = "!some-room-id:example.org";
const reason = "some reason";
@@ -272,6 +347,7 @@ describe("MatrixClient", function () {
expect((await prom).room_id).toBe(roomId);
});
// eslint-disable-next-line @vitest/expect-expect
it("should no-op if you've already knocked a room", function () {
const room = new Room(roomId, client, userId);
@@ -305,23 +381,16 @@ describe("MatrixClient", function () {
[
403,
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
],
[
500,
{ errcode: "INTERNAL_SERVER_ERROR" },
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
"MatrixError: [403] You don't have permission to knock",
],
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
];
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
const prom = client.knockRoom(roomId);
await Promise.all([
httpBackend.flushAllExpected(),
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
]);
await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
});
});
});
@@ -644,126 +713,6 @@ describe("MatrixClient", function () {
});
});
describe("downloadKeys", function () {
if (!CRYPTO_ENABLED) {
return;
}
beforeEach(function () {
// running initLegacyCrypto should trigger a key upload
httpBackend.when("POST", "/keys/upload").respond(200, {});
return Promise.all([client.initLegacyCrypto(), httpBackend.flush("/keys/upload", 1)]);
});
afterEach(() => {
client.stopClient();
});
it("should do an HTTP request and then store the keys", function () {
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
const borisKeys = {
dev1: {
algorithms: ["1"],
device_id: "dev1",
keys: { "ed25519:dev1": ed25519key },
signatures: {
boris: {
"ed25519:dev1":
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
},
},
unsigned: { abc: "def" },
user_id: "boris",
},
};
const chazKeys = {
dev2: {
algorithms: ["2"],
device_id: "dev2",
keys: { "ed25519:dev2": ed25519key },
signatures: {
chaz: {
"ed25519:dev2":
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
},
},
unsigned: { ghi: "def" },
user_id: "chaz",
},
};
/*
function sign(o) {
var anotherjson = require('another-json');
var b = JSON.parse(JSON.stringify(o));
delete(b.signatures);
delete(b.unsigned);
return client.crypto.olmDevice.sign(anotherjson.stringify(b));
};
logger.log("Ed25519: " + ed25519key);
logger.log("boris:", sign(borisKeys.dev1));
logger.log("chaz:", sign(chazKeys.dev2));
*/
httpBackend
.when("POST", "/keys/query")
.check(function (req) {
expect(req.data).toEqual({
device_keys: {
boris: [],
chaz: [],
},
});
})
.respond(200, {
device_keys: {
boris: borisKeys,
chaz: chazKeys,
},
});
const prom = client.downloadKeys(["boris", "chaz"]).then(function (res) {
assertObjectContains(res.get("boris")!.get("dev1")!, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { abc: "def" },
});
assertObjectContains(res.get("chaz")!.get("dev2")!, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
unsigned: { ghi: "def" },
});
});
httpBackend.flush("");
return prom;
});
});
describe("deleteDevice", function () {
const auth = { identifier: 1 };
it("should pass through an auth dict", function () {
httpBackend
.when("DELETE", "/_matrix/client/v3/devices/my_device")
.check(function (req) {
expect(req.data).toEqual({ auth: auth });
})
.respond(200);
const prom = client.deleteDevice("my_device", auth);
httpBackend.flush("");
return prom;
});
});
describe("partitionThreadedEvents", function () {
let room: Room;
beforeEach(() => {
@@ -1243,7 +1192,7 @@ describe("MatrixClient", function () {
describe("logout", () => {
it("should abort pending requests when called with stopClient=true", async () => {
httpBackend.when("POST", "/logout").respond(200, {});
const fn = jest.fn();
const fn = vi.fn();
client.http.request(Method.Get, "/test").catch(fn);
client.logout(true);
await httpBackend.flush(undefined);
@@ -1371,7 +1320,7 @@ describe("MatrixClient", function () {
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
it("should always fetch capabilities and then cache", async () => {
@@ -1442,6 +1391,7 @@ describe("MatrixClient", function () {
});
describe("publicRooms", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should use GET request if no server or filter is specified", () => {
httpBackend.when("GET", "/publicRooms").respond(200, {});
client.publicRooms({});
@@ -1628,52 +1578,9 @@ describe("MatrixClient", function () {
});
});
describe("uploadKeys", () => {
// uploadKeys() is a no-op nowadays, so there's not much to test here.
it("should complete successfully", async () => {
await client.uploadKeys();
});
});
describe("getCryptoTrustCrossSignedDevices", () => {
it("should throw if e2e is disabled", () => {
expect(() => client.getCryptoTrustCrossSignedDevices()).toThrow("End-to-end encryption disabled");
});
it("should proxy to the crypto backend", async () => {
const mockBackend = {
getTrustCrossSignedDevices: jest.fn().mockReturnValue(true),
} as unknown as Mocked<CryptoBackend>;
client["cryptoBackend"] = mockBackend;
expect(client.getCryptoTrustCrossSignedDevices()).toBe(true);
mockBackend.getTrustCrossSignedDevices.mockReturnValue(false);
expect(client.getCryptoTrustCrossSignedDevices()).toBe(false);
});
});
describe("setCryptoTrustCrossSignedDevices", () => {
it("should throw if e2e is disabled", () => {
expect(() => client.setCryptoTrustCrossSignedDevices(false)).toThrow("End-to-end encryption disabled");
});
it("should proxy to the crypto backend", async () => {
const mockBackend = {
setTrustCrossSignedDevices: jest.fn(),
} as unknown as Mocked<CryptoBackend>;
client["cryptoBackend"] = mockBackend;
client.setCryptoTrustCrossSignedDevices(true);
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(true);
client.setCryptoTrustCrossSignedDevices(false);
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(false);
});
});
describe("setSyncPresence", () => {
it("should pass calls through to the underlying sync api", () => {
const setPresence = jest.fn();
const setPresence = vi.fn();
// @ts-ignore
client.syncApi = { setPresence };
client.setSyncPresence(SetPresence.Unavailable);
@@ -1682,6 +1589,7 @@ describe("MatrixClient", function () {
});
describe("sendTyping", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should bail early for guests", async () => {
client.setGuest(true);
await client.sendTyping("!room:server", true, 100);
@@ -1937,6 +1845,28 @@ describe("MatrixClient", function () {
expect(client.getUserIdLocalpart()).toBe("alice");
});
});
describe("setRoomMutePushRule", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should set room push rule to muted", async () => {
const roomId = "!roomId:server";
const client = new MatrixClient({
baseUrl: "http://localhost",
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
});
client.pushRules = {
global: {
room: [{ rule_id: roomId, actions: [], default: false, enabled: false }],
},
};
const path = `/pushrules/global/room/${encodeURIComponent(roomId)}`;
httpBackend.when("DELETE", path).respond(200, {});
httpBackend.when("PUT", path).respond(200, {});
client.setRoomMutePushRule("global", roomId, true);
await httpBackend.flush("");
});
});
});
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
@@ -2197,11 +2127,3 @@ const buildEventCreate = () =>
type: "m.room.create",
unsigned: { age: 80126105 },
});
function assertObjectContains(obj: Record<string, any>, expected: any): void {
for (const k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
}
}
+107 -2
View File
@@ -5,7 +5,7 @@ import { ClientEvent, MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store";
import { type IStore } from "../../src/store";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient opts", function () {
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
await expect(
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
).rejects.toThrow("MatrixError: [500] Unknown message");
).rejects.toThrow("MatrixError: [500] Ruh roh");
});
it("shouldn't queue events", async () => {
@@ -205,4 +205,109 @@ describe("MatrixClient opts", function () {
expect(res.event_id).toEqual("foo");
});
});
describe("with opts.queryParams", function () {
let client: MatrixClient;
let httpBackend: HttpBackend;
const userId = "@rsb-tbg:localhost";
beforeEach(function () {
httpBackend = new HttpBackend();
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
queryParams: { user_id: userId },
});
});
afterEach(function () {
client.stopClient();
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
it("should include queryParams in matrix server requests", async () => {
const eventId = "$test:event";
httpBackend
.when("PUT", "/txn1")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {
event_id: eventId,
});
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "test message", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual(eventId);
});
it("should include queryParams in sync requests", async () => {
httpBackend
.when("GET", "/versions")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {});
httpBackend
.when("GET", "/pushrules")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {});
httpBackend
.when("POST", "/filter")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, { filter_id: "foo" });
httpBackend
.when("GET", "/sync")
.check((req) => {
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, syncData);
client.startClient();
await httpBackend.flush("/versions", 1);
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
});
it("should merge queryParams with request-specific params", async () => {
const eventId = "$test:event";
httpBackend
.when("PUT", "/txn1")
.check((req) => {
// Should contain both global queryParams and request-specific params
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
return true;
})
.respond(200, {
event_id: eventId,
});
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "test message", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual(eventId);
});
});
});
+2 -3
View File
@@ -15,9 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
import type HttpBackend from "matrix-mock-request";
import { Direction, type MatrixClient, MatrixScheduler } from "../../src/matrix";
import { TestClient } from "../TestClient";
describe("MatrixClient relations", () => {
+2 -3
View File
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import { EventStatus, MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix";
import type HttpBackend from "matrix-mock-request";
import { EventStatus, type MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
@@ -14,20 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import type HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import {
MatrixError,
ClientEvent,
IEvent,
MatrixClient,
type IEvent,
type MatrixClient,
RoomEvent,
ISyncResponse,
IMinimalEvent,
IRoomEvent,
Room,
type ISyncResponse,
type IMinimalEvent,
type IRoomEvent,
type Room,
} from "../../src";
import { TestClient } from "../TestClient";
import { KnownMembership } from "../../src/@types/membership";
@@ -721,7 +720,7 @@ describe("MatrixClient room timelines", function () {
} else {
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
}
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Vitest? */);
room.on(RoomEvent.TimelineReset, async () => {
try {
+10 -12
View File
@@ -15,9 +15,9 @@ limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
const makeQueryablePromise = <T = void>(promise: Promise<T>) => {
let resolved = false;
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
});
it("should retry, until errors are solved.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
vi.useFakeTimers();
fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
@@ -105,19 +104,18 @@ describe("MatrixClient syncing errors", () => {
await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[1].promise).toBe(SyncState.Error);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
});
it("should stop sync keep alive when client is stopped.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
vi.useFakeTimers();
fetchMock
.get("end:capabilities", {})
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
@@ -146,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
const syntState = await firstSyncEvent.promise;
expect(syntState).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
vi.runAllTimers(); // this will skip forward to trigger the keepAlive
jest.useRealTimers(); // we need real timer for the setTimout below to work
vi.useRealTimers(); // we need real timer for the setTimout below to work
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
+26 -21
View File
@@ -16,8 +16,7 @@ limitations under the License.
import "fake-indexeddb/auto";
import HttpBackend from "matrix-mock-request";
import type HttpBackend from "matrix-mock-request";
import {
EventTimeline,
MatrixEvent,
@@ -25,16 +24,15 @@ import {
RoomStateEvent,
RoomMemberEvent,
UNSTABLE_MSC2716_MARKER,
MatrixClient,
type MatrixClient,
ClientEvent,
IndexedDBCryptoStore,
ISyncResponse,
IRoomEvent,
IJoinedRoom,
IStateEvent,
IMinimalEvent,
type ISyncResponse,
type IRoomEvent,
type IJoinedRoom,
type IStateEvent,
type IMinimalEvent,
NotificationCountType,
IEphemeral,
type IEphemeral,
Room,
IndexedDBStore,
RelationType,
@@ -47,9 +45,16 @@ import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IActionsObject } from "../../src/pushprocessor";
import { type IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
a: {};
b: {};
}
}
describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
@@ -89,6 +94,7 @@ describe("MatrixClient syncing", () => {
presence: {},
};
// eslint-disable-next-line @vitest/expect-expect
it("should /sync after /pushrules and /filter.", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
@@ -112,7 +118,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";
@@ -227,7 +233,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";
@@ -496,7 +502,7 @@ describe("MatrixClient syncing", () => {
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
return httpBackend!.flushAllExpected();
@@ -989,7 +995,7 @@ describe("MatrixClient syncing", () => {
roomVersion: "org.matrix.msc2716v3",
},
].forEach((testMeta) => {
// eslint-disable-next-line jest/valid-title
// eslint-disable-next-line @vitest/valid-title
describe(testMeta.label, () => {
const roomCreateEvent = utils.mkEvent({
type: "m.room.create",
@@ -1830,7 +1836,7 @@ describe("MatrixClient syncing", () => {
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
room!.hasEncryptionStateEvent = vi.fn().mockReturnValue(true);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
@@ -2514,7 +2520,7 @@ describe("MatrixClient syncing", () => {
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
client!.store.storeAccountDataEvents([eventA1, eventB1]);
const fn = jest.fn();
const fn = vi.fn();
client!.on(ClientEvent.AccountData, fn);
httpBackend!.when("GET", "/sync").respond(200, {
@@ -2564,16 +2570,15 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
};
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
cryptoStore: new IndexedDBCryptoStore(globalThis.indexedDB, "tests"),
});
// rust crypto uses by default indexeddb
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const idbHttpBackend = idbTestClient.httpBackend;
const idbClient = idbTestClient.client;
idbHttpBackend.when("GET", "/versions").respond(200, {});
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";
@@ -16,14 +16,13 @@ limitations under the License.
import "fake-indexeddb/auto";
import HttpBackend from "matrix-mock-request";
import type HttpBackend from "matrix-mock-request";
import {
Category,
ClientEvent,
EventType,
ISyncResponse,
MatrixClient,
type ISyncResponse,
type MatrixClient,
MatrixEvent,
NotificationCountType,
RelationType,
@@ -63,7 +62,7 @@ describe("Notification count fixing", () => {
client!.startClient({ threadSupport: true });
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const event = new MatrixEvent({
room_id: roomId,
@@ -78,7 +77,7 @@ describe("Notification count fixing", () => {
},
});
jest.spyOn(event, "getPushActions").mockReturnValue({
vi.spyOn(event, "getPushActions").mockReturnValue({
notify: true,
tweaks: {},
});
@@ -124,7 +123,7 @@ describe("MatrixClient syncing", () => {
]);
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
@@ -144,7 +143,7 @@ describe("MatrixClient syncing", () => {
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
let lastEvent: MatrixEvent | null = null;
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
vi.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
lastEvent = event as MatrixEvent;
return { event_id: reactionEventId };
});
@@ -196,7 +195,7 @@ describe("MatrixClient syncing", () => {
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
await httpBackend!.flushAllExpected();
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
import { mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "@fetch-mock/vitest";
import {
MSC4108FailureReason,
@@ -26,22 +25,21 @@ import {
PayloadType,
RendezvousError,
} from "../../../src/rendezvous";
import { defer } from "../../../src/utils";
import {
ClientPrefix,
DEVICE_CODE_SCOPE,
IHttpOpts,
IMyDevice,
MatrixClient,
type IHttpOpts,
type IMyDevice,
type MatrixClient,
MatrixError,
MatrixHttpApi,
} from "../../../src";
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
const baseUrl = "https://example.com";
const crypto = {
exportSecretsForQrLogin: jest.fn(),
exportSecretsForQrLogin: vi.fn(),
};
const client = {
doesServerSupportUnstableFeature(feature: string) {
@@ -55,9 +53,9 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
},
baseUrl,
getDomain: () => "example.com",
getDevice: jest.fn(),
getCrypto: jest.fn(() => crypto),
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
getDevice: vi.fn(),
getCrypto: vi.fn(() => crypto),
getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
} as unknown as MatrixClient;
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
baseUrl: client.baseUrl,
@@ -69,10 +67,6 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
describe("MSC4108SignInWithQR", () => {
beforeEach(() => {
fetchMock.get(
"https://issuer/.well-known/openid-configuration",
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
);
fetchMock.get("https://issuer/jwks", {
status: 200,
headers: {
@@ -82,10 +76,6 @@ describe("MSC4108SignInWithQR", () => {
});
});
afterEach(() => {
fetchMock.reset();
});
const url = "https://fallbackserver/rz/123";
const deviceId = "DEADB33F";
const verificationUri = "https://example.com/verify";
@@ -116,17 +106,17 @@ describe("MSC4108SignInWithQR", () => {
let opponentLogin: MSC4108SignInWithQR;
beforeEach(async () => {
let ourData = defer<string>();
let opponentData = defer<string>();
let ourData = Promise.withResolvers<string>();
let opponentData = Promise.withResolvers<string>();
const ourMockSession = {
send: jest.fn(async (newData) => {
send: vi.fn(async (newData) => {
ourData.resolve(newData);
}),
receive: jest.fn(() => {
receive: vi.fn(() => {
const prom = opponentData.promise;
prom.then(() => {
opponentData = defer();
opponentData = Promise.withResolvers();
});
return prom;
}),
@@ -139,13 +129,13 @@ describe("MSC4108SignInWithQR", () => {
},
} as unknown as MSC4108RendezvousSession;
const opponentMockSession = {
send: jest.fn(async (newData) => {
send: vi.fn(async (newData) => {
opponentData.resolve(newData);
}),
receive: jest.fn(() => {
receive: vi.fn(() => {
const prom = ourData.promise;
prom.then(() => {
ourData = defer();
ourData = Promise.withResolvers();
});
return prom;
}),
@@ -156,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
const ourChannel = new MSC4108SecureChannel(ourMockSession);
const qrCodeData = QrCodeData.fromBytes(
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
await ourChannel.generateCode(QrCodeIntent.Reciprocate, client.getDomain()!),
);
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
@@ -176,7 +166,7 @@ describe("MSC4108SignInWithQR", () => {
it("should be able to connect with opponent and share verificationUri", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
@@ -199,7 +189,7 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device already exists", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
@@ -249,12 +239,12 @@ describe("MSC4108SignInWithQR", () => {
// @ts-ignore
await opponentLogin.receive();
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
const payload = {
secrets: expect.objectContaining(secrets),
@@ -266,13 +256,13 @@ describe("MSC4108SignInWithQR", () => {
});
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
});
jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
vi.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
@@ -285,7 +275,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
const ourProm = ourLogin.shareSecrets();
await expect(ourProm).rejects.toThrow("New device not found");
@@ -302,7 +292,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(
vi.mocked(client.getDevice).mockRejectedValue(
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
);
@@ -319,7 +309,7 @@ describe("MSC4108SignInWithQR", () => {
});
it("should not send secrets if user cancels", async () => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
@@ -338,16 +328,16 @@ describe("MSC4108SignInWithQR", () => {
// @ts-ignore
await opponentLogin.receive();
const deferred = defer<IMyDevice>();
mocked(client.getDevice).mockReturnValue(deferred.promise);
const deviceResolvers = Promise.withResolvers<IMyDevice>();
vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
deferred.resolve({} as IMyDevice);
deviceResolvers.resolve({} as IMyDevice);
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
await Promise.all([
expect(ourProm).rejects.toThrow("User cancelled"),
+80 -58
View File
@@ -15,56 +15,70 @@ limitations under the License.
*/
// eslint-disable-next-line no-restricted-imports
import MockHttpBackend from "matrix-mock-request";
import { fail } from "assert";
import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Extension } from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src";
import type MockHttpBackend from "matrix-mock-request";
import {
MatrixClient,
MatrixEvent,
SlidingSync,
SlidingSyncEvent,
type MSC3575RoomData,
SlidingSyncState,
type Extension,
} from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
import {
type MatrixClient,
type MatrixEvent,
NotificationCountType,
JoinRule,
MatrixError,
EventType,
IPushRules,
type IPushRules,
PushRuleKind,
TweakName,
ClientEvent,
RoomMemberEvent,
RoomEvent,
Room,
IRoomTimelineData,
type Room,
type IRoomTimelineData,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncApiOptions, SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src";
import { type SyncApiOptions, SyncState } from "../../src/sync";
import { type IStoredClientOpts } from "../../src";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership";
import { type SyncCryptoCallbacks } from "../../src/common-crypto/CryptoBackend";
declare module "../../src/@types/event" {
interface AccountDataEvents {
global_test: {};
tester: {};
}
}
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;
let sdk: SlidingSyncSdk | undefined;
let mockSlidingSync: SlidingSync | undefined;
let syncCryptoCallback: SyncCryptoCallbacks | undefined;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getListParams = jest.fn();
s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn();
s.setList = jest.fn();
s.setListRanges = jest.fn();
s.start = jest.fn();
s.stop = jest.fn();
s.resend = jest.fn();
s.getListParams = vi.fn();
s.getListData = vi.fn();
s.getRoomSubscriptions = vi.fn();
s.modifyRoomSubscriptionInfo = vi.fn();
s.modifyRoomSubscriptions = vi.fn();
s.registerExtension = vi.fn();
s.setList = vi.fn();
s.setListRanges = vi.fn();
s.start = vi.fn();
s.stop = vi.fn();
s.resend = vi.fn();
return s;
};
@@ -97,7 +111,7 @@ describe("SlidingSyncSdk", () => {
expect(m.getType()).toEqual(want[i].type);
expect(m.getSender()).toEqual(want[i].sender);
expect(m.getId()).toEqual(want[i].event_id);
expect(m.getContent()).toEqual(want[i].content);
expect(m.getContent<IContent>()).toEqual(want[i].content);
expect(m.getTs()).toEqual(want[i].origin_server_ts);
if (want[i].unsigned) {
expect(m.getUnsigned()).toEqual(want[i].unsigned);
@@ -112,15 +126,16 @@ describe("SlidingSyncSdk", () => {
// assign client/httpBackend globals
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
testOpts = testOpts || {};
const syncOpts: SyncApiOptions = {};
const syncOpts: SyncApiOptions = { logger };
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
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);
@@ -135,7 +150,7 @@ describe("SlidingSyncSdk", () => {
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension<any, any> => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
// find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
@@ -353,7 +368,7 @@ describe("SlidingSyncSdk", () => {
});
it("can be created with live events", async () => {
const seenLiveEventDeferred = defer<boolean>();
const seenLiveEventDeferred = Promise.withResolvers<boolean>();
const listener = (
ev: MatrixEvent,
room?: Room,
@@ -633,43 +648,38 @@ 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({
it("gets enabled all the time", async () => {
expect(await ext.onRequest(true)).toEqual({
enabled: true,
});
expect(await ext.onRequest(false)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("can update device lists", () => {
client!.crypto!.processDeviceLists = jest.fn();
syncCryptoCallback!.processDeviceLists = vi.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 = vi.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",
]);
});
@@ -686,11 +696,13 @@ describe("SlidingSyncSdk", () => {
ext = findExtension("account_data");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
it("gets enabled all the time", async () => {
expect(await ext.onRequest(true)).toEqual({
enabled: true,
});
expect(await ext.onRequest(false)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes global account data", async () => {
@@ -710,7 +722,7 @@ describe("SlidingSyncSdk", () => {
});
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeTruthy();
expect(globalData.getContent()).toEqual(globalContent);
expect(globalData.getContent<IContent>()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
@@ -745,7 +757,7 @@ describe("SlidingSyncSdk", () => {
expect(room).toBeTruthy();
const event = room.getAccountData(roomType)!;
expect(event).toBeTruthy();
expect(event.getContent()).toEqual(roomContent);
expect(event.getContent<IContent>()).toEqual(roomContent);
});
it("doesn't crash for unknown room account data", async () => {
@@ -814,8 +826,12 @@ describe("SlidingSyncSdk", () => {
ext = findExtension("to_device");
});
it("gets enabled with a limit on the initial request only", () => {
const reqJson: any = ext.onRequest(true);
it("gets enabled all the time", async () => {
let reqJson: any = await ext.onRequest(true);
expect(reqJson.enabled).toEqual(true);
expect(reqJson.limit).toBeGreaterThan(0);
expect(reqJson.since).toBeUndefined();
reqJson = await ext.onRequest(false);
expect(reqJson.enabled).toEqual(true);
expect(reqJson.limit).toBeGreaterThan(0);
expect(reqJson.since).toBeUndefined();
@@ -826,11 +842,12 @@ describe("SlidingSyncSdk", () => {
next_batch: "12345",
events: [],
});
expect(ext.onRequest(false)).toEqual({
expect(await ext.onRequest(false)).toMatchObject({
since: "12345",
});
});
// eslint-disable-next-line @vitest/expect-expect
it("can handle missing fields", async () => {
ext.onResponse({
next_batch: "23456",
@@ -845,7 +862,7 @@ describe("SlidingSyncSdk", () => {
};
let called = false;
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent);
expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType);
called = true;
});
@@ -910,11 +927,13 @@ describe("SlidingSyncSdk", () => {
ext = findExtension("typing");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
it("gets enabled all the time", async () => {
expect(await ext.onRequest(true)).toEqual({
enabled: true,
});
expect(await ext.onRequest(false)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes typing notifications", async () => {
@@ -1033,11 +1052,13 @@ describe("SlidingSyncSdk", () => {
ext = findExtension("receipts");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
it("gets enabled all the time", async () => {
expect(await ext.onRequest(true)).toEqual({
enabled: true,
});
expect(await ext.onRequest(false)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes receipts", async () => {
@@ -1075,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
expect(receipt?.data.thread_id).toBeFalsy();
});
// eslint-disable-next-line @vitest/expect-expect
it("gracefully handles missing rooms when receiving receipts", async () => {
const roomId = "!room:id";
const alice = "@alice:alice";
+37 -754
View File
@@ -15,21 +15,20 @@ limitations under the License.
*/
// eslint-disable-next-line no-restricted-imports
import EventEmitter from "events";
import MockHttpBackend from "matrix-mock-request";
import type EventEmitter from "events";
import type MockHttpBackend from "matrix-mock-request";
import {
SlidingSync,
SlidingSyncState,
ExtensionState,
SlidingSyncEvent,
Extension,
SlidingSyncEventHandlerMap,
MSC3575RoomData,
type Extension,
type SlidingSyncEventHandlerMap,
type MSC3575RoomData,
} from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
import { MatrixClient } from "../../src";
import { type MatrixClient } from "../../src";
/**
* Tests for sliding sync. These tests are broken down into sub-tests which are reliant upon one another.
@@ -42,7 +41,7 @@ describe("SlidingSync", () => {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const proxyBaseUrl = "http://localhost:8008";
const syncUrl = proxyBaseUrl + "/_matrix/client/unstable/org.matrix.msc3575/sync";
const syncUrl = proxyBaseUrl + "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync";
// assign client/httpBackend globals
const setupClient = () => {
@@ -83,6 +82,7 @@ describe("SlidingSync", () => {
await p;
});
// eslint-disable-next-line @vitest/expect-expect
it("should stop the sync loop upon calling stop()", () => {
slidingSync.stop();
httpBackend!.verifyNoOutstandingExpectation();
@@ -104,8 +104,8 @@ describe("SlidingSync", () => {
};
const ext: Extension<any, any> = {
name: () => "custom_extension",
onRequest: (initial) => {
return { initial: initial };
onRequest: async (_) => {
return { initial: true };
},
onResponse: async (res) => {
return;
@@ -144,18 +144,16 @@ describe("SlidingSync", () => {
});
await httpBackend!.flushAllExpected();
// expect nothing but ranges and non-initial extensions to be sent
// expect all params to be sent TODO: check MSC4186
httpBackend!
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists["a"]).toEqual({
ranges: [[0, 10]],
});
expect(body.lists["a"]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: false });
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams!["pos"]).toEqual("11");
})
.respond(200, function () {
@@ -333,6 +331,7 @@ describe("SlidingSync", () => {
await p;
});
// TODO: this does not exist in MSC4186
it("should be able to unsubscribe from a room", async () => {
httpBackend!
.when("POST", syncUrl)
@@ -390,18 +389,19 @@ describe("SlidingSync", () => {
[3, 5],
];
// request first 3 rooms
const listReq = {
ranges: [[0, 2]],
sort: ["by_name"],
timeline_limit: 1,
required_state: [["m.room.topic", ""]],
filters: {
is_dm: true,
},
};
let slidingSync: SlidingSync;
it("should be possible to subscribe to a list", async () => {
// request first 3 rooms
const listReq = {
ranges: [[0, 2]],
sort: ["by_name"],
timeline_limit: 1,
required_state: [["m.room.topic", ""]],
filters: {
is_dm: true,
},
};
slidingSync = new SlidingSync(proxyBaseUrl, new Map([["a", listReq]]), {}, client!, 1);
httpBackend!
.when("POST", syncUrl)
@@ -453,11 +453,6 @@ describe("SlidingSync", () => {
expect(slidingSync.getListData("b")).toBeNull();
const syncData = slidingSync.getListData("a")!;
expect(syncData.joinedCount).toEqual(500); // from previous test
expect(syncData.roomIndexToRoomId).toEqual({
0: roomA,
1: roomB,
2: roomC,
});
});
it("should be possible to adjust list ranges", async () => {
@@ -468,10 +463,9 @@ describe("SlidingSync", () => {
const body = req.data;
logger.log("next ranges", body.lists["a"].ranges);
expect(body.lists).toBeTruthy();
expect(body.lists["a"]).toEqual({
// only the ranges should be sent as the rest are unchanged and sticky
ranges: newRanges,
});
// list range should be changed
listReq.ranges = newRanges;
expect(body.lists["a"]).toEqual(listReq); // resend all values TODO: check MSC4186
})
.respond(200, {
pos: "b",
@@ -496,7 +490,9 @@ describe("SlidingSync", () => {
await httpBackend!.flushAllExpected();
await responseProcessed;
// setListRanges for an invalid list key returns an error
await expect(slidingSync.setListRanges("idontexist", newRanges)).rejects.toBeTruthy();
expect(() => {
slidingSync.setListRanges("idontexist", newRanges);
}).toThrow();
});
it("should be possible to add an extra list", async () => {
@@ -514,10 +510,7 @@ describe("SlidingSync", () => {
const body = req.data;
logger.log("extra list", body);
expect(body.lists).toBeTruthy();
expect(body.lists["a"]).toEqual({
// only the ranges should be sent as the rest are unchanged and sticky
ranges: newRanges,
});
expect(body.lists["a"]).toEqual(listReq); // resend all values TODO: check MSC4186
expect(body.lists["b"]).toEqual(extraListReq);
})
.respond(200, {
@@ -538,16 +531,6 @@ describe("SlidingSync", () => {
},
},
});
listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("b");
expect(joinedCount).toEqual(50);
expect(roomIndexToRoomId).toEqual({
0: roomA,
1: roomB,
2: roomC,
});
return true;
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
@@ -555,706 +538,6 @@ describe("SlidingSync", () => {
await httpBackend!.flushAllExpected();
await responseProcessed;
});
it("should be possible to get list DELETE/INSERTs", async () => {
// move C (2) to A (0)
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e",
lists: {
a: {
count: 500,
ops: [
{
op: "DELETE",
index: 2,
},
{
op: "INSERT",
index: 0,
room_id: roomC,
},
],
},
b: {
count: 50,
},
},
});
let listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomA,
2: roomB,
});
return true;
},
);
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
// move C (0) back to A (2)
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f",
lists: {
a: {
count: 500,
ops: [
{
op: "DELETE",
index: 0,
},
{
op: "INSERT",
index: 2,
room_id: roomC,
},
],
},
b: {
count: 50,
},
},
});
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomA,
1: roomB,
2: roomC,
});
return true;
});
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should ignore invalid list indexes", async () => {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e",
lists: {
a: {
count: 500,
ops: [
{
op: "DELETE",
index: 2324324,
},
],
},
b: {
count: 50,
},
},
});
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomA,
1: roomB,
2: roomC,
});
return true;
},
);
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should be possible to update a list", async () => {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g",
lists: {
a: {
count: 42,
ops: [
{
op: "INVALIDATE",
range: [0, 2],
},
{
op: "SYNC",
range: [0, 1],
room_ids: [roomB, roomC],
},
],
},
b: {
count: 50,
},
},
});
// update the list with a new filter
slidingSync.setList("a", {
filters: {
is_encrypted: true,
},
ranges: [[0, 100]],
});
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(42);
expect(roomIndexToRoomId).toEqual({
0: roomB,
1: roomC,
});
return true;
},
);
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
// this refers to a set of operations where the end result is no change.
it("should handle net zero operations correctly", async () => {
const indexToRoomId = {
0: roomB,
1: roomC,
};
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it
lists: {
a: {
count: 500,
ops: [
{
op: "DELETE",
index: 2,
},
{
op: "INSERT",
index: 0,
room_id: roomA,
},
{
op: "DELETE",
index: 0,
},
],
},
b: {
count: 50,
},
},
});
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual(indexToRoomId);
return true;
},
);
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle deletions correctly", async () => {
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomB,
1: roomC,
});
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g",
lists: {
a: {
count: 499,
ops: [
{
op: "DELETE",
index: 0,
},
],
},
b: {
count: 50,
},
},
});
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(499);
expect(roomIndexToRoomId).toEqual({
0: roomC,
});
return true;
},
);
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle insertions correctly", async () => {
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomC,
});
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h",
lists: {
a: {
count: 500,
ops: [
{
op: "INSERT",
index: 1,
room_id: roomA,
},
],
},
b: {
count: 50,
},
},
});
let listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomA,
});
return true;
},
);
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h",
lists: {
a: {
count: 501,
ops: [
{
op: "INSERT",
index: 1,
room_id: roomB,
},
],
},
b: {
count: 50,
},
},
});
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(501);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomB,
2: roomA,
});
return true;
});
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
slidingSync.stop();
});
// Regression test to make sure things like DELETE 0 INSERT 0 work correctly and we don't
// end up losing room IDs.
it("should handle insertions with a spurious DELETE correctly", async () => {
slidingSync = new SlidingSync(
proxyBaseUrl,
new Map([
[
"a",
{
ranges: [[0, 20]],
},
],
]),
{},
client!,
1,
);
// initially start with nothing
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "a",
lists: {
a: {
count: 0,
ops: [],
},
},
});
slidingSync.start();
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({});
// insert a room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "b",
lists: {
a: {
count: 1,
ops: [
{
op: "DELETE",
index: 0,
},
{
op: "INSERT",
index: 0,
room_id: roomA,
},
],
},
},
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomA,
});
// insert another room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c",
lists: {
a: {
count: 1,
ops: [
{
op: "DELETE",
index: 1,
},
{
op: "INSERT",
index: 0,
room_id: roomB,
},
],
},
},
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomB,
1: roomA,
});
// insert a final room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c",
lists: {
a: {
count: 1,
ops: [
{
op: "DELETE",
index: 2,
},
{
op: "INSERT",
index: 0,
room_id: roomC,
},
],
},
},
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomC,
1: roomB,
2: roomA,
});
slidingSync.stop();
});
});
describe("transaction IDs", () => {
beforeAll(setupClient);
afterAll(teardownClient);
const roomId = "!foo:bar";
let slidingSync: SlidingSync;
// really this applies to them all but it's easier to just test one
it("should resolve modifyRoomSubscriptions after SlidingSync.start() is called", async () => {
const roomSubInfo = {
timeline_limit: 1,
required_state: [["m.room.name", ""]],
};
// add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
// modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomId]).toEqual(roomSubInfo);
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
})
.respond(200, function () {
return {
pos: "aaa",
txn_id: txnId,
lists: {},
extensions: {},
rooms: {
[roomId]: {
name: "foo bar",
required_state: [],
timeline: [],
},
},
};
});
slidingSync.start();
await httpBackend!.flushAllExpected();
await subscribePromise;
});
it("should resolve setList during a connection", async () => {
const newList = {
ranges: [[0, 20]],
};
const promise = slidingSync.setList("a", newList);
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists["a"]).toEqual(newList);
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
})
.respond(200, function () {
return {
pos: "bbb",
txn_id: txnId,
lists: { a: { count: 5 } },
extensions: {},
};
});
await httpBackend!.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges("a", [[20, 40]]);
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists["a"]).toEqual({
ranges: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
})
.respond(200, function () {
return {
pos: "ccc",
txn_id: txnId,
lists: { a: { count: 5 } },
extensions: {},
};
});
await httpBackend!.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should resolve modifyRoomSubscriptionInfo during a connection", async () => {
const promise = slidingSync.modifyRoomSubscriptionInfo({
timeline_limit: 99,
});
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomId]).toEqual({
timeline_limit: 99,
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
})
.respond(200, function () {
return {
pos: "ddd",
txn_id: txnId,
extensions: {},
};
});
await httpBackend!.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
const gotTxnIds: any[] = [];
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data.txn_id);
};
const failPromise = slidingSync.setListRanges("a", [[20, 40]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
await httpBackend!.flushAllExpected();
const failPromise2 = slidingSync.setListRanges("a", [[60, 70]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend!.flushAllExpected();
const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
.check((req) => {
txnId = req.data.txn_id;
})
.respond(200, () => {
// include the txn_id, earlier requests should now be reject()ed.
return {
pos: "g",
txn_id: txnId,
};
});
await Promise.all([
expect(failPromise).rejects.toEqual(gotTxnIds[0]),
expect(failPromise2).rejects.toEqual(gotTxnIds[1]),
httpBackend!.flushAllExpected(),
okPromise,
]);
expect(txnId).toBeDefined();
});
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
const gotTxnIds: any[] = [];
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data?.txn_id);
};
const A = slidingSync.setListRanges("a", [[20, 40]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
await httpBackend!.flushAllExpected();
const B = slidingSync.setListRanges("a", [[60, 70]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
await httpBackend!.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
const C = slidingSync.setListRanges("a", [[0, 20]]);
let pendingC = true;
C.finally(() => {
pendingC = false;
});
httpBackend!
.when("POST", syncUrl)
.check(pushTxn)
.respond(200, () => {
// include the txn_id for B, so C's promise is outstanding
return {
pos: "C",
txn_id: gotTxnIds[1],
};
});
await Promise.all([
expect(A).rejects.toEqual(gotTxnIds[0]),
httpBackend!.flushAllExpected(),
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]), // B is resolved
]);
expect(pendingC).toBe(true); // C is pending still
});
it("should do nothing for unknown txn_ids", async () => {
const promise = slidingSync.setListRanges("a", [[20, 40]]);
let pending = true;
promise.finally(() => {
pending = false;
});
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists["a"]).toEqual({
ranges: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
})
.respond(200, function () {
return {
pos: "ccc",
txn_id: "bogus transaction id",
lists: { a: { count: 5 } },
extensions: {},
};
});
await httpBackend!.flushAllExpected();
expect(txnId).toBeDefined();
expect(pending).toBe(true);
slidingSync.stop();
});
});
describe("custom room subscriptions", () => {
@@ -1544,7 +827,7 @@ describe("SlidingSync", () => {
const extPre: Extension<any, any> = {
name: () => preExtName,
onRequest: (initial) => {
onRequest: async (initial) => {
return onPreExtensionRequest(initial);
},
onResponse: (res) => {
@@ -1554,7 +837,7 @@ describe("SlidingSync", () => {
};
const extPost: Extension<any, any> = {
name: () => postExtName,
onRequest: (initial) => {
onRequest: async (initial) => {
return onPostExtensionRequest(initial);
},
onResponse: (res) => {
@@ -1569,7 +852,7 @@ describe("SlidingSync", () => {
const callbackOrder: string[] = [];
let extensionOnResponseCalled = false;
onPreExtensionRequest = () => {
onPreExtensionRequest = async () => {
return extReq;
};
onPreExtensionResponse = async (resp) => {
@@ -1609,7 +892,7 @@ describe("SlidingSync", () => {
});
it("should be able to send nothing in an extension request/response", async () => {
onPreExtensionRequest = () => {
onPreExtensionRequest = async () => {
return undefined;
};
let responseCalled = false;
@@ -1644,7 +927,7 @@ describe("SlidingSync", () => {
it("is possible to register extensions after start() has been called", async () => {
slidingSync.registerExtension(extPost);
onPostExtensionRequest = () => {
onPostExtensionRequest = async () => {
return extReq;
};
let responseCalled = false;
-27
View File
@@ -1,27 +0,0 @@
/*
Copyright 2017 Vector creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../src/logger";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
globalThis.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available", e);
}
+16 -2
View File
@@ -14,8 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
jest.mock("../src/http-api/utils", () => ({
...jest.requireActual("../src/http-api/utils"),
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
vi.mock("../src/http-api/utils", async () => ({
...(await vi.importActual("../src/http-api/utils")),
// We mock timeoutSignal otherwise it causes tests to leave timers running
timeoutSignal: () => new AbortController().signal,
}));
manageFetchMockGlobally();
beforeEach(() => {
fetchMock.hardReset();
fetchMock.mockGlobal();
});
// Don't make test fail too soon due to timeouts while debugging.
if (process.env.VSCODE_INSPECTOR_OPTIONS) {
vi.setConfig({ testTimeout: 60 * 1000 * 5 }); // 5 minutes
}
+73 -48
View File
@@ -14,85 +14,110 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { ISyncResponder } from "./SyncResponder";
import { type ISyncResponder } from "./SyncResponder";
/**
* An object which intercepts `account_data` get and set requests via fetch-mock.
* An object which intercepts `account_data` get and set requests via fetch-mock.
*
* To use this, call {@link interceptSetAccountData} for each type of account date that should be handled. The updated
* account data will be stored in {@link accountDataEvents}; it will also trigger a sync response echoing the updated
* data.
*
* Optionally, you can also call {@link interceptGetAccountData}.
*/
export class AccountDataAccumulator {
/**
* The account data events to be returned by the sync.
* Will be updated when fetchMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
* Will be used by `sendSyncResponseWithUpdatedAccountData`
*/
public accountDataEvents: Map<string, any> = new Map();
public constructor(private syncResponder: ISyncResponder) {}
private accountDataResolvers = new Map<string, PromiseWithResolvers<any>>();
private setInterceptRunning = false;
/**
* Intercept requests to set a particular type of account data.
* Intercept setting of account data.
*
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
* resolved.
*
* @param accountDataType - type of account data to be intercepted
* @param opts - options to pass to fetchMock
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public interceptSetAccountData(
accountDataType: string,
opts?: Parameters<(typeof fetchMock)["put"]>[2],
): Promise<any> {
return new Promise((resolve) => {
// Called when the cross signing key is uploaded
fetchMock.put(
`express:/_matrix/client/v3/user/:userId/account_data/${accountDataType}`,
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
const type = url.split("/").pop();
// update account data for sync response
this.accountDataEvents.set(type!, content);
resolve(content);
return {};
},
opts,
);
public interceptSetAccountData(): void {
if (this.setInterceptRunning) return;
this.setInterceptRunning = true;
fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
const content = JSON.parse(callLog.options.body as string);
const type = callLog.url.split("/").pop();
// update account data for sync response
this.accountDataEvents.set(type!, content);
this.accountDataResolvers.get(type!)?.resolve(content);
if (!this.accountDataResolvers.delete(type!)) {
// Check for a wildcard matcher
for (const [key, resolver] of this.accountDataResolvers) {
if (key.endsWith("*") && type?.startsWith(key.slice(0, -1))) {
resolver.resolve(content);
this.accountDataResolvers.delete(key);
}
}
}
// return a sync response
this.sendSyncResponseWithUpdatedAccountData();
return {};
});
}
/**
* Wait for a particular type of account data.
*
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
* resolved.
*
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public waitForAccountData(type: string): Promise<any> {
const resolvers = Promise.withResolvers<any>();
this.accountDataResolvers.set(type, resolvers);
this.interceptSetAccountData();
return resolvers.promise;
}
/**
* Intercept all requests to get account data
*/
public interceptGetAccountData(): void {
fetchMock.get(
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
(url) => {
const type = url.split("/").pop();
const existing = this.accountDataEvents.get(type!);
if (existing) {
// return it
return {
status: 200,
body: existing,
};
} else {
// 404
return {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
};
}
},
{ overwriteRoutes: true },
);
fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
const type = callLog.url.split("/").pop();
const existing = this.accountDataEvents.get(type!);
if (existing) {
// return it
return {
status: 200,
body: existing,
};
} else {
// 404
return {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
};
}
});
}
/**
* Send a sync response the current account data events.
*/
public sendSyncResponseWithUpdatedAccountData(syncResponder: ISyncResponder): void {
private sendSyncResponseWithUpdatedAccountData(): void {
try {
syncResponder.sendOrQueueSyncResponse({
this.syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: Array.from(this.accountDataEvents, ([type, content]) => ({
+102 -12
View File
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import debugFunc, { type Debugger } from "debug";
import fetchMock from "@fetch-mock/vitest";
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
import type { CrossSigningKeyInfo } from "../../src/crypto-api";
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
*
@@ -55,28 +56,53 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
private readonly debug: Debugger;
private deviceKeys: IDeviceKeys | null = null;
private crossSigningKeys: CrossSigningKeys | null = null;
private oneTimeKeys: Record<string, IOneTimeKey> = {};
private readonly oneTimeKeysPromise: Promise<void>;
/**
* Construct a new E2EKeyReceiver.
*
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
* It will immediately register an intercept of [`/keys/upload`][1], [`/keys/signatures/upload`][2] and
* [`/keys/device_signing/upload`][3] requests for the given homeserverUrl.
* Only requests made to this server will be intercepted: this allows a single test to use more than one
* client and have the keys collected separately.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
* [1]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keysupload
* [2]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keyssignaturesupload
* [3]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keysdevice_signingupload
*
* @param homeserverUrl - The Homeserver Url of the client under test.
* @param routeNamePrefix - An optional prefix to add to the fetchmock route names. Required if there is more than
* one E2EKeyReceiver instance active.
*/
public constructor(homeserverUrl: string) {
public constructor(homeserverUrl: string, routeNamePrefix: string = "") {
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
// set up a listener for /keys/upload.
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
const listener = (url: string, options: RequestInit) =>
this.onKeyUploadRequest(resolveOneTimeKeys, options);
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
fetchMock.post(
new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
{ name: routeNamePrefix + "keys-upload" },
);
});
fetchMock.post(
new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
(callLog) => this.onSignaturesUploadRequest(callLog.options),
{
name: routeNamePrefix + "upload-sigs",
},
);
fetchMock.post(
new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
(callLog) => this.onSigningKeyUploadRequest(callLog.options),
{
name: routeNamePrefix + "upload-cross-signing-keys",
},
);
}
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
@@ -87,8 +113,10 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
if (this.deviceKeys) {
throw new Error("Application attempted to upload E2E device keys multiple times");
}
this.debug(`received device keys`);
this.deviceKeys = content.device_keys;
this.debug(
`received device keys for user ID ${this.deviceKeys!.user_id}, device ID ${this.deviceKeys!.device_id}`,
);
}
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
@@ -113,6 +141,47 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
};
}
private async onSignaturesUploadRequest(request: RequestInit): Promise<object> {
const content = JSON.parse(request.body as string) as KeySignatures;
for (const [userId, userKeys] of Object.entries(content)) {
for (const [deviceId, signedKey] of Object.entries(userKeys)) {
this.onDeviceSignatureUpload(userId, deviceId, signedKey);
}
}
return {};
}
private onDeviceSignatureUpload(userId: string, deviceId: string, signedKey: CrossSigningKeyInfo | ISignedKey) {
if (!this.deviceKeys || userId != this.deviceKeys.user_id || deviceId != this.deviceKeys.device_id) {
this.debug(
`Ignoring device key signature upload for unknown device user ID ${userId}, device ID ${deviceId}`,
);
return;
}
this.debug(`received device key signature for user ID ${userId}, device ID ${deviceId}`);
this.deviceKeys.signatures ??= {};
for (const [signingUser, signatures] of Object.entries(signedKey.signatures!)) {
this.deviceKeys.signatures[signingUser] = Object.assign(
this.deviceKeys.signatures[signingUser] ?? {},
signatures,
);
}
}
private async onSigningKeyUploadRequest(request: RequestInit): Promise<object> {
const content = JSON.parse(request.body as string);
if (this.crossSigningKeys) {
throw new Error("Application attempted to upload E2E cross-signing keys multiple times");
}
this.debug(`received cross-signing keys`);
// Remove UIA data
delete content["auth"];
this.crossSigningKeys = content;
return {};
}
/** Get the uploaded Ed25519 key
*
* If device keys have not yet been uploaded, throws an error
@@ -150,6 +219,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
return this.deviceKeys;
}
/**
* If cross-signing keys have been uploaded, return them. Else return null.
*/
public getUploadedCrossSigningKeys(): CrossSigningKeys | null {
return this.crossSigningKeys;
}
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
@@ -161,4 +237,18 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
await this.oneTimeKeysPromise;
return this.oneTimeKeys;
}
/**
* If no one-time keys have yet been uploaded, return `null`.
* Otherwise, pop a key from the uploaded list.
*/
public getOneTimeKey(): [string, IOneTimeKey] | null {
const keys = Object.entries(this.oneTimeKeys);
if (keys.length == 0) {
return null;
}
const [otkId, otk] = keys[0];
delete this.oneTimeKeys[otkId];
return [otkId, otk];
}
}
+28 -20
View File
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils";
import { IDownloadKeyResult } from "../../src";
import { IDeviceKeys } from "../../src/@types/crypto";
import { E2EKeyReceiver } from "./E2EKeyReceiver";
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
import { type IDeviceKeys } from "../../src/@types/crypto";
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
/**
* An object which intercepts `/keys/query` fetches via fetch-mock.
@@ -42,26 +42,23 @@ export class E2EKeyResponder {
*/
public constructor(homeserverUrl: string) {
// set up a listener for /keys/query.
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), (callLog) =>
this.onKeyQueryRequest(callLog.options),
);
}
private onKeyQueryRequest(options: RequestInit) {
const content = JSON.parse(options.body as string);
const usersToReturn = Object.keys(content["device_keys"]);
const response = {
device_keys: {} as { [userId: string]: any },
master_keys: {} as { [userId: string]: any },
self_signing_keys: {} as { [userId: string]: any },
user_signing_keys: {} as { [userId: string]: any },
failures: {} as { [serverName: string]: any },
};
device_keys: {},
master_keys: {},
self_signing_keys: {},
user_signing_keys: {},
failures: {},
} as IDownloadKeyResult;
for (const user of usersToReturn) {
const userKeys = this.deviceKeysByUserByDevice.get(user);
if (userKeys !== undefined) {
response.device_keys[user] = Object.fromEntries(userKeys.entries());
}
// First see if we have an E2EKeyReceiver for this user, and if so, return any keys that have been uploaded
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
if (e2eKeyReceiver !== undefined) {
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
@@ -69,16 +66,27 @@ export class E2EKeyResponder {
response.device_keys[user] ??= {};
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
}
const crossSigningKeys = e2eKeyReceiver.getUploadedCrossSigningKeys();
if (crossSigningKeys !== null) {
response.master_keys![user] = crossSigningKeys["master_key"];
response.self_signing_keys![user] = crossSigningKeys["self_signing_key"] as SigningKeys;
}
}
// Mix in any keys that have been added explicitly to this E2EKeyResponder.
const userKeys = this.deviceKeysByUserByDevice.get(user);
if (userKeys !== undefined) {
response.device_keys[user] ??= {};
Object.assign(response.device_keys[user], Object.fromEntries(userKeys.entries()));
}
if (this.masterKeysByUser.hasOwnProperty(user)) {
response.master_keys[user] = this.masterKeysByUser[user];
response.master_keys![user] = this.masterKeysByUser[user];
}
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
response.self_signing_keys![user] = this.selfSigningKeysByUser[user];
}
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
response.user_signing_keys![user] = this.userSigningKeysByUser[user];
}
}
return response;
+74
View File
@@ -0,0 +1,74 @@
/*
Copyright 2025 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils";
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
import { type IClaimKeysRequest } from "../../src";
/**
* An object which intercepts `/keys/claim` fetches via fetch-mock.
*/
export class E2EOTKClaimResponder {
private e2eKeyReceiversByUserByDevice = new MapWithDefault<string, Map<string, E2EKeyReceiver>>(() => new Map());
/**
* Construct a new E2EOTKClaimResponder.
*
* It will immediately register an intercept of `/keys/claim` requests for the given homeserverUrl.
* Only /claim requests made to this server will be intercepted: this allows a single test to use more than one
* client and have the keys collected separately.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
this.onKeyClaimRequest(callLog.options),
);
}
private onKeyClaimRequest(options: RequestInit) {
const content = JSON.parse(options.body as string) as IClaimKeysRequest;
const response = {
one_time_keys: {} as { [userId: string]: any },
};
for (const [userId, devices] of Object.entries(content["one_time_keys"])) {
for (const deviceId of Object.keys(devices)) {
const e2eKeyReceiver = this.e2eKeyReceiversByUserByDevice.get(userId)?.get(deviceId);
const otk = e2eKeyReceiver?.getOneTimeKey();
if (otk) {
const [keyId, key] = otk;
response.one_time_keys[userId] ??= {};
response.one_time_keys[userId][deviceId] = {
[keyId]: key,
};
}
}
}
return response;
}
/**
* Add an E2EKeyReceiver to poll for uploaded keys
*
* When the `/keys/claim` request is received, a OTK will be removed from the `E2EKeyReceiver` and
* added to the response.
*/
public addKeyReceiver(userId: string, deviceId: string, e2eKeyReceiver: E2EKeyReceiver) {
this.e2eKeyReceiversByUserByDevice.getOrCreate(userId).set(deviceId, e2eKeyReceiver);
}
}
+5 -5
View File
@@ -15,9 +15,9 @@ limitations under the License.
*/
import debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import FetchMock from "fetch-mock";
import { type Debugger } from "debug";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients
*
@@ -75,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (callLog) =>
this.onSyncRequest(),
);
}
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
private async onSyncRequest(): Promise<RouteResponse> {
switch (this.state) {
case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready");
+1 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { MatrixEvent } from "../../src";
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType } from "../../src/@types/location";
import { type LocationAssetType } from "../../src/@types/location";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
type InfoContentProps = {
+24 -18
View File
@@ -14,12 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
import { type MockedObject } from "vitest";
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
import { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { User } from "../../src/models/user";
// Cribbed from https://github.com/jestjs/jest/blob/94830794dc5dfca1b49bc435b7b031b27838a798/packages/jest-mock/src/index.ts
type FunctionLike = (...args: any) => any;
type MethodLikeKeys<T> = keyof {
[K in keyof T as Required<T>[K] extends FunctionLike ? K : never]: T[K];
};
/**
* Mock client with real event emitter
* useful for testing code that listens
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
/**
* - make a mock client
* - cast the type to mocked(MatrixClient)
* - cast the type to vi.mocked(MatrixClient)
* - spy on MatrixClientPeg.get to return the mock
* eg
* ```
* const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
getUserId: vi.fn().mockReturnValue(aliceId),
});
* ```
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
const mock = vi.mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
return mock;
};
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
* ```
*/
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
getUserId: vi.fn().mockReturnValue(userId),
getSafeUserId: vi.fn().mockReturnValue(userId),
getUser: vi.fn().mockReturnValue(new User(userId)),
isGuest: vi.fn().mockReturnValue(false),
mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: vi.fn(),
});
/**
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
* ```
*/
export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(),
getPushActionsForEvent: jest.fn(),
decryptEventIfNeeded: vi.fn(),
getPushActionsForEvent: vi.fn(),
});
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCachedCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
getIdentityServerUrl: vi.fn(),
getHomeserverUrl: vi.fn(),
getCachedCapabilities: vi.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false),
});
+4 -2
View File
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MockInstance } from "vitest";
/**
* Filter emitter.emit mock calls to find relevant events
* eg:
* ```
* const emitSpy = jest.spyOn(state, 'emit');
* const emitSpy = vi.spyOn(state, 'emit');
* << actions >>
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
export const filterEmitCallsByEventType = (eventType: string, spy: MockInstance<(...args: any[]) => any>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+4 -6
View File
@@ -14,12 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
// Vitest lacks tickAsync() and a number of other async methods which break the event loop,
// letting scheduled promise callbacks run. So we have to do it manually
// (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
// hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
export function flushPromises() {
return new Promise((r) => {
+36 -49
View File
@@ -14,39 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { KeyBackupInfo } from "../../src/crypto-api";
import { type KeyBackupInfo } from "../../src/crypto-api";
/**
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
*
* @param homeserverUrl - the homeserver url for the client under test
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
*/
export function mockInitialApiRequests(homeserverUrl: string) {
fetchMock.getOnce(
new URL("/_matrix/client/versions", homeserverUrl).toString(),
{ versions: ["v1.1"] },
{ overwriteRoutes: true },
);
fetchMock.getOnce(
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
{},
{ overwriteRoutes: true },
);
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
fetchMock.postOnce(
new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(),
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
{ filter_id: "fid" },
);
fetchMock.getOnce(
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter/fid`, homeserverUrl).toString(),
{ filter_id: "fid" },
{ overwriteRoutes: true },
);
}
/**
* Mock the requests needed to set up cross signing
* Mock the requests needed to set up cross signing, besides those provided by {@link E2EKeyReceiver}.
*
* Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
*/
export function mockSetupCrossSigningRequests(): void {
// have account_data requests return an empty object
@@ -54,19 +48,6 @@ export function mockSetupCrossSigningRequests(): void {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
});
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// ... and one to upload the cross-signing keys (with UIA)
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
{},
);
}
/**
@@ -79,24 +60,30 @@ export function mockSetupCrossSigningRequests(): void {
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
*/
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
},
},
});
{ name: "room-keys-version" },
);
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
return {
version: backupVersion,
};
});
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(callLog) => {
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.modifyRoute("room-keys-version", { response: backupData });
return {
version: backupVersion,
};
},
{ name: "post-room-keys-version" },
);
}
+1 -39
View File
@@ -14,42 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (
issuer = "https://auth.org/",
additionalGrantTypes: string[] = [],
): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
code_challenge_methods_supported: ["S256"],
});
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";
+26 -17
View File
@@ -84,9 +84,9 @@ def main() -> None:
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
@@ -246,15 +246,6 @@ export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult>
/** Signed OTKs, returned by `POST /keys/claim` */
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
/** base64-encoded backup decryption (private) key */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
/** Backup decryption key in export format */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
json.dumps(set_of_exported_room_keys, indent=4)
@@ -278,6 +269,23 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
/** base64-encoded backup decryption (private) key */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
/** Backup decryption key in export format */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
/**
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
*/
export const {prefix}PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {{
[{prefix}MEGOLM_SESSION_DATA.session_id]: {prefix}CURVE25519_KEY_BACKUP_DATA
}};
"""
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
@@ -385,7 +393,7 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
"""
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
that can be imported via importRoomKeys API.
that can be imported via importRoomKeys API, or shared via MSC4268 room history sharing.
Returns the exported key, the matching privat edKey (needed to encrypt)
"""
index = 0
@@ -409,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
"session_id": encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
),
"session_key": encode_base64(exported_key),
"session_key": encode_base64(bytes(exported_key)),
"sender_claimed_keys": {
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
},
"forwarding_curve25519_key_chain": [],
"m.shared_history": True,
}
return megolm_export, private_key
@@ -458,7 +467,7 @@ def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed2
"room_id": "!room:id",
"sender_key": previous["sender_key"],
"session_id": previous["session_id"],
"session_key": encode_base64(exported_key),
"session_key": encode_base64(bytes(exported_key)),
"sender_claimed_keys": previous["sender_claimed_keys"],
"forwarding_curve25519_key_chain": [],
}
@@ -609,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
message += signature
cipher_text = encode_base64(message)
cipher_text = encode_base64(bytes(message))
encrypted_payload = {
"algorithm" : "m.megolm.v1.aes-sha2",
@@ -653,7 +662,7 @@ def export_recovery_key(key_b64: str) -> str:
export_bytes += parity_byte.to_bytes(1, 'big')
# The byte string is encoded using base58
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
recovery_key = base58.b58encode(bytes(export_bytes)).decode('utf-8')
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
return ' '.join(split)
+76 -51
View File
@@ -3,9 +3,9 @@
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import { IDownloadKeyResult, IEvent } from "../../../src";
import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup";
import type { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import type { IDownloadKeyResult, IEvent } from "../../../src";
import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
@@ -118,26 +118,6 @@ export const ONE_TIME_KEYS = {
}
};
/** base64-encoded backup decryption (private) key */
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Backup decryption key in export format */
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
}
}
}
};
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
},
{
"algorithm": "m.megolm.v1.aes-sha2",
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
}
];
@@ -174,7 +156,8 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
"sender_claimed_keys": {
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
};
/** A ratcheted version of MEGOLM_SESSION_DATA */
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"forwarded_count": 0,
"is_verified": false,
"session_data": {
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d/r0NTff1SQt+1GopZkT0nq6jF5Wh/oX+8iwtYjHvTxMpN1UQoXAvRF40O+EVg+Q3efJXh1t45cMco8EWU64VerOir+k7cQ3C9FtcgQw3kmz3s3HeVY10o13X/w6+rc8n6vXqxuIxYHnFxanxX8B6TgTMZNajNfVsmJV0aC1aezim7E2gsftc+6+zW5G+rCFaEsWV/IuSOUz0+Hh0U+7hzSrz9/4qXPEVmPy1f6Ll4hhquPAlXPVDwddqlJDYj7kmvzr1g3bKVpk+TtKDbWlVQDPaJx2DEI2jGkPYjhYb7okpTFKpUny94dZmFIQqCeSGPIniaq8Y+/CanugQ1ZRVQcThuXrTewqWhXcpVvkVHT9i4ImcpBl95HzCBXuiwSUv6FKvO25fp++w555rbn2piFtilrUwnkrZPW32jFuaQcKZF4mZwcLeH7POL5UCuS4TWyaKyArp7bRzXwWuIq1wPET2nAMUmUVL7ge2+tAevk1WOIsjLgSaz/g55wO3Yma7yhXRFKcnzTjS0hUQOZ3GfTNwCM4pjzAtIPzvVd4Fp0b1emWZS5WyOYdXsceEDi3c6WtkoHWOKhPU0zBzn8hA9TdlFFqKzf2QFbN5Zgg0gprDLnLWgpc3/ieI4C7ndEQ7ZeTNMXbT/Y10APFk3qO+IGkLXJ97/qTF41EXFDhlsL0",
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
"mac": "OibmACbORhI"
}
@@ -229,6 +212,37 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** base64-encoded backup decryption (private) key that matches the public key in CURVE25519_KEY_BACKUP_DATA */
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** base64-encoded backup decryption (private) key that does not match the public key in CURVE25519_KEY_BACKUP_DATA */
export const BACKUP_DECRYPTION_KEY_BASE64_ALT = "dh4fP2LITyJusgnb0dEq/SQK253WGObvLxXF5FEX6qc";
/** Backup decryption key in export format */
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
}
}
}
};
/**
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
* Contains the key from MEGOLM_SESSION_DATA.
*/
export const PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
[MEGOLM_SESSION_DATA.session_id]: CURVE25519_KEY_BACKUP_DATA
};
// Bob data
export const BOB_TEST_USER_ID = "@bob:xyz";
@@ -338,26 +352,6 @@ export const BOB_ONE_TIME_KEYS = {
}
};
/** base64-encoded backup decryption (private) key */
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Backup decryption key in export format */
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
}
}
}
};
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{
@@ -369,7 +363,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
},
{
"algorithm": "m.megolm.v1.aes-sha2",
@@ -380,7 +375,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
}
];
@@ -394,7 +390,8 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"sender_claimed_keys": {
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
};
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
@@ -416,7 +413,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"forwarded_count": 0,
"is_verified": false,
"session_data": {
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAA7TEpHlx8Ks23hCqXmVW710VjqK2K9xnWCyJvkHfE8x0w6AYvffDj+tRVP8C8M7t4849rD2itn0uma+YMkvjG/nANUTxG1dBf3oUOZ673vflCPoaz7s7x9ZNhYDVSVH5JTdMgNwwN42R5dqqxnGTu516tJzJh/9BWvyD9oIPWJ8X0rt1sbzEJ3PZeBXcSy8GTlZ1SgSFjeiXlwYxOZCaX2sxprk4N1oI1db6g+wCDBhbCGGucJIlTDJna/h9/C5J4drGd/fkisG3SidUmJXXCyInhs/BhwjGAtTGeQS8j7R8UnJxhMulYBHSckzj0Kas71LElPp8W8M4Jq81APA03n5UfYB+U6jbxjDgf8OJnxGQyrteq9F2+SEvS/TwHe1pE3t6EM2mDYRoYDTpU5pTNYSJkGIQMfWJKRxxuWUGs29o1twewJ6dhHgm+SlCII0M7ESoVdV54vxZCvHZnPcR0NXDzal7ils7zBKJmamHfPQBuaqNPU3KmSo+5R8ngFPaWU5LbWqYp/WxSBfNCoLZ7Jf8Io5uitjXTATR2qy2r6l/RJmk3RlfP51kliQqI2TWqRF96oaB96IGgUGSFCX/2pv0psOBGc1SjfmMB3d7gYis+2iBYVbG3xmnpeXbqvlD0Lw9TiTIPkjhJkTW1+lXyhy1xVH9ZmcFamcL7bX15Jx",
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
"mac": "lEfHlqfJQwU"
}
@@ -449,6 +446,34 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** base64-encoded backup decryption (private) key */
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Backup decryption key in export format */
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
}
}
}
};
/**
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
* Contains the key from BOB_MEGOLM_SESSION_DATA.
*/
export const BOB_PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
[BOB_MEGOLM_SESSION_DATA.session_id]: BOB_CURVE25519_KEY_BACKUP_DATA
};
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
+158 -37
View File
@@ -1,25 +1,32 @@
import mkdebug from "debug";
// eslint-disable-next-line no-restricted-imports
import EventEmitter from "events";
// load olm before the sdk if possible
import "../olm-loader";
import { logger } from "../../src/logger";
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import type EventEmitter from "events";
import {
type IContent,
type IEvent,
type IEventRelation,
type IUnsigned,
MatrixEvent,
MatrixEventEvent,
} from "../../src/models/event";
import {
ClientEvent,
EventType,
IJoinedRoom,
IPusher,
ISyncResponse,
MatrixClient,
HistoryVisibility,
type IJoinedRoom,
type IPusher,
type ISyncResponse,
type MatrixClient,
MsgType,
RelationType,
} from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
import { TEST_ROOM_ID } from "./test-data";
import { KnownMembership, Membership } from "../../src/@types/membership";
import { KnownMembership, type Membership } from "../../src/@types/membership";
const debug = mkdebug("test-utils");
/**
* Return a promise that is resolved when the client next emits a
@@ -35,7 +42,7 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
const p = new Promise<void>((resolve) => {
const cb = (state: SyncState) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
debug(`syncPromise(${count}): ${state}`);
if (state === SyncState.Syncing) {
resolve();
} else {
@@ -51,13 +58,22 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
}
/**
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
* @param roomMembers
* @param roomId
* Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
* and history visibility set to `shared`.
*
* @returns the sync response
* @param roomMembers - An array of user IDs representing the members of the room.
* @param roomHistoryVisibility - The history visibility setting for the room. Defaults to `shared`.
* @param roomId - The ID of the room. Defaults to `TEST_ROOM_ID`.
* @param encryptStateEvents - A boolean indicating whether state events should be encrypted. Defaults to `false`.
*
* @returns The sync response object containing the room data.
*/
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse {
export function getSyncResponse(
roomMembers: string[],
roomHistoryVisibility: HistoryVisibility = HistoryVisibility.Shared,
roomId = TEST_ROOM_ID,
encryptStateEvents = false,
): ISyncResponse {
const roomResponse: IJoinedRoom = {
summary: {
"m.heroes": [],
@@ -71,7 +87,16 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
"algorithm": "m.megolm.v1.aes-sha2",
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
},
}),
mkEventCustom({
sender: roomMembers[0],
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: roomHistoryVisibility,
},
}),
],
@@ -125,7 +150,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
// eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
result[key] = vi.fn();
}
} catch {
// Direct access to some non-function fields of DOM prototypes may
@@ -516,25 +541,25 @@ export async function awaitDecryption(
// already
if (event.getClearContent() !== null) {
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
debug(`event ${event.getId()} got decryption error; waiting`);
} else {
return event;
}
} else {
logger.log(`${Date.now()}: event ${event.getId()} is not yet decrypted; waiting`);
debug(`event ${event.getId()} is not yet decrypted; waiting`);
}
return new Promise((resolve) => {
if (waitOnDecryptionFailure) {
event.on(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
debug(`MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
if (!err) {
resolve(ev);
}
});
} else {
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
debug(`MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
resolve(ev);
});
}
@@ -552,20 +577,21 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
...extra,
});
/**
* a list of the supported crypto implementations, each with a callback to initialise that implementation
* for the given client
*/
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (globalThis.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initLegacyCrypto();
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
/**
* Counts the number of times that an event was emitted.
*/
export class EventCounter {
public counter;
constructor(emitter: EventEmitter, event: string) {
this.counter = 0;
emitter.on(event, () => {
this.counter++;
});
}
}
/**
* Advance the fake timers in a loop until the given promise resolves or rejects.
*
@@ -580,8 +606,103 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
});
while (!resolved) {
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
}
return await promise;
}
export function jestFakeTimersAreEnabled(): boolean {
return Object.prototype.hasOwnProperty.call(setTimeout, "clock");
}
/**
* Run `callback` in a loop, until it returns a successful result (i.e. it does not throw), or we reach a timeout
*
* Based on the function of the same name in the {@link https://testing-library.com/docs/dom-testing-library/api-async/#waitfor DOM testing library}.
*
* @param callback - The function to call to check if we can proceed. If it returns a result (including a falsey one),
* `waitFor` returns that result. If it throws, `waitFor` continues to wait.
*
* May return a promise, in which case no further checks are done until the promise resolves.
*
* @param timeout - The time to wait for, overall, in ms. If `callback` still hasn't returned a successful result after
* this time, `waitFor` will throw an error.
*
* Defaults to 1000.
*
* @param interval - How often to call `callback`. Defaults to 50.
*/
export function waitFor<T>(
callback: () => Promise<T> | T,
{
timeout = 1000,
interval = 50,
}: {
timeout?: number;
interval?: number;
} = {},
): Promise<T> {
return new Promise((resolve, reject) => {
let lastError: any;
let finished = false;
let intervalId: ReturnType<typeof setTimeout> | undefined;
let promisePending = false;
const overallTimeoutTimer = setTimeout(handleTimeout, timeout);
const usingJestFakeTimers = jestFakeTimersAreEnabled();
if (usingJestFakeTimers) {
checkCallback();
while (!finished) {
vi.advanceTimersByTime(interval);
// Could have timed-out
if (finished) break;
checkCallback();
}
} else {
intervalId = setInterval(checkCallback, interval);
checkCallback();
}
function checkCallback() {
if (promisePending) {
// still waiting for the previous check
return;
}
async function doCheck() {
try {
const result = await callback();
onDone();
resolve(result);
} catch (error) {
// Save the most recent callback error to reject the promise with it in the event of a timeout
lastError = error;
}
}
promisePending = true;
doCheck().finally(() => {
promisePending = false;
});
}
function onDone(): void {
finished = true;
clearTimeout(overallTimeoutTimer);
if (intervalId !== undefined) clearInterval(intervalId);
}
function handleTimeout() {
onDone();
if (lastError) {
reject(lastError);
} else {
reject(new Error("Timed out in waitFor."));
}
}
});
}
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DumpDataSetInfo } from "../index";
import { type DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DumpDataSetInfo } from "../index";
import { type DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
import { DumpDataSetInfo } from "../index";
import { type KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
import { type DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { DumpDataSetInfo } from "../index";
import { type DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
+4 -4
View File
@@ -15,10 +15,10 @@ limitations under the License.
*/
import { RelationType } from "../../src/@types/event";
import { MatrixClient } from "../../src/client";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { type MatrixClient } from "../../src/client";
import { type MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { type Room } from "../../src/models/room";
import { type Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { mkMessage } from "./test-utils";
export const makeThreadEvent = ({
+65 -105
View File
@@ -15,32 +15,29 @@ limitations under the License.
*/
import {
ClientEvent,
ClientEventHandlerMap,
type ClientEvent,
type ClientEventHandlerMap,
EventType,
GroupCall,
type GroupCall,
GroupCallIntent,
GroupCallType,
IContent,
ISendEventResponse,
MatrixClient,
MatrixEvent,
Room,
type IContent,
type MatrixClient,
type MatrixEvent,
RoomMember,
RoomState,
type RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
SendToDeviceContentMap,
type RoomStateEventHandlerMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
import { SyncState } from "../../src/sync";
import { CallEvent, CallEventHandlerMap, CallState, MatrixCall } from "../../src/webrtc/call";
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
import { CallFeed } from "../../src/webrtc/callFeed";
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
import { type CallEvent, type CallEventHandlerMap, CallState, type MatrixCall } from "../../src/webrtc/call";
import { type CallEventHandlerEvent, type CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
import { type CallFeed } from "../../src/webrtc/callFeed";
import { type GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
import { type GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
import { type IScreensharingOpts, type MediaHandler } from "../../src/webrtc/mediaHandler";
export const DUMMY_SDP =
"v=0\r\n" +
@@ -268,19 +265,20 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
public setCodecPreferences = vi.fn<RTCRtpTransceiver["setCodecPreferences"]>();
}
export class MockMediaStreamTrack {
export class MockMediaStreamTrack extends EventTarget {
constructor(
public readonly id: string,
public readonly kind: "audio" | "video",
public enabled = true,
) {}
) {
super();
}
public stop = jest.fn<void, []>();
public stop = vi.fn<() => void>();
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public settings?: MediaTrackSettings;
@@ -288,45 +286,21 @@ export class MockMediaStreamTrack {
return this.settings!;
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public typed(): MediaStreamTrack {
return this as unknown as MediaStreamTrack;
}
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
export class MockMediaStream {
export class MockMediaStream extends EventTarget {
constructor(
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
) {
super();
}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public getTracks() {
return this.tracks;
}
@@ -336,17 +310,9 @@ export class MockMediaStream {
public getVideoTracks() {
return this.tracks.filter((track) => track.kind === "video");
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public addTrack(track: MockMediaStreamTrack) {
this.tracks.push(track);
this.dispatchEvent("addtrack");
this.dispatchEvent(new Event("addtrack"));
}
public removeTrack(track: MockMediaStreamTrack) {
this.tracks.splice(this.tracks.indexOf(track), 1);
@@ -390,7 +356,7 @@ export class MockMediaHandler {
public stopUserMediaStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
public getScreensharingStream = vi.fn((opts?: IScreensharingOpts) => {
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
@@ -415,19 +381,19 @@ export class MockMediaHandler {
}
export class MockMediaDevices {
public enumerateDevices = jest
.fn<Promise<MediaDeviceInfo[]>, []>()
public enumerateDevices = vi
.fn<MediaDevices["enumerateDevices"]>()
.mockResolvedValue([
new MockMediaDeviceInfo("audioinput").typed(),
new MockMediaDeviceInfo("videoinput").typed(),
]);
public getUserMedia = jest
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
public getUserMedia = vi
.fn<MediaDevices["getUserMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
public getDisplayMedia = jest
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
public getDisplayMedia = vi
.fn<MediaDevices["getDisplayMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
public typed(): MediaDevices {
@@ -461,14 +427,8 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
calls: new Map<string, MatrixCall>(),
};
public sendStateEvent = jest.fn<
Promise<ISendEventResponse>,
[roomId: string, eventType: EventType, content: any, statekey: string]
>();
public sendToDevice = jest.fn<
Promise<{}>,
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
>();
public sendStateEvent = vi.fn<MatrixClient["sendStateEvent"]>();
public sendToDevice = vi.fn<MatrixClient["sendToDevice"]>();
public isInitialSyncComplete(): boolean {
return false;
@@ -498,11 +458,11 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getUseE2eForGroupCall = () => false;
public checkTurnServers = () => null;
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
public getSyncState = vi.fn<MatrixClient["getSyncState"]>().mockReturnValue(SyncState.Syncing);
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public getFoci = jest.fn();
public getRooms = vi.fn<MatrixClient["getRooms"]>().mockReturnValue([]);
public getRoom = vi.fn();
public getFoci = vi.fn();
public supportsThreads(): boolean {
return true;
@@ -533,20 +493,20 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
isAudioMuted: jest.fn().mockReturnValue(false),
isVideoMuted: jest.fn().mockReturnValue(false),
setAudioVideoMuted: vi.fn<CallFeed["setAudioVideoMuted"]>(),
isAudioMuted: vi.fn().mockReturnValue(false),
isVideoMuted: vi.fn().mockReturnValue(false),
stream: new MockMediaStream("stream"),
} as unknown as CallFeed;
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public initStats = jest.fn<void, []>();
public reject = vi.fn<() => void>();
public answerWithCallFeeds = vi.fn<MatrixCall["answerWithCallFeeds"]>();
public hangup = vi.fn<() => void>();
public initStats = vi.fn<() => void>();
public sendMetadataUpdate = jest.fn<void, []>();
public sendMetadataUpdate = vi.fn<() => void>();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
@@ -585,11 +545,11 @@ export class MockCallFeed {
}
export function installWebRTCMocks() {
globalThis.navigator = {
vi.stubGlobal("navigator", {
mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator;
});
globalThis.window = {
vi.stubGlobal("window", {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
@@ -597,16 +557,16 @@ export function installWebRTCMocks() {
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
globalThis.document = {};
});
vi.stubGlobal("document", {});
// @ts-ignore Mock
globalThis.AudioContext = MockAudioContext;
// @ts-ignore Mock
globalThis.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
getCapabilities: vi.fn().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
@@ -614,7 +574,7 @@ export function installWebRTCMocks() {
// @ts-ignore Mock
globalThis.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
getCapabilities: vi.fn().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
@@ -631,22 +591,22 @@ export function makeMockGroupCallStateEvent(
redacted?: boolean,
): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue(content),
getStateKey: jest.fn().mockReturnValue(groupCallId),
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
getType: vi.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: vi.fn().mockReturnValue(roomId),
getTs: vi.fn().mockReturnValue(0),
getContent: vi.fn().mockReturnValue(content),
getStateKey: vi.fn().mockReturnValue(groupCallId),
isRedacted: vi.fn().mockReturnValue(redacted ?? false),
} as unknown as MatrixEvent;
}
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue({}),
getStateKey: jest.fn().mockReturnValue(groupCallId),
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: vi.fn().mockReturnValue(roomId),
getTs: vi.fn().mockReturnValue(0),
getContent: vi.fn().mockReturnValue({}),
getStateKey: vi.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
+2 -2
View File
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
});
it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable");
const ns = new NamespacedValue("stable", null);
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]);
@@ -61,7 +61,7 @@ describe("UnstableValue", () => {
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null!, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(<any>ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
+2 -2
View File
@@ -38,7 +38,7 @@ describe("ReEmitter", function () {
const src = new EventSource();
const tgt = new EventTarget();
const handler = jest.fn();
const handler = vi.fn();
tgt.on(EVENTNAME, handler);
const reEmitter = new ReEmitter(tgt);
@@ -61,7 +61,7 @@ describe("ReEmitter", function () {
// without the workaround in ReEmitter, this would throw
src.doAnError();
const handler = jest.fn();
const handler = vi.fn();
tgt.on("error", handler);
src.doAnError();
+16 -16
View File
@@ -1,11 +1,11 @@
import { ConnectionError } from "../../src/http-api/errors";
import { ClientEvent, MatrixClient, Store } from "../../src/client";
import { ClientEvent, type MatrixClient, type Store } from "../../src/client";
import { ToDeviceMessageQueue } from "../../src/ToDeviceMessageQueue";
import { getMockClientWithEventEmitter } from "../test-utils/client";
import { StubStore } from "../../src/store/stub";
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { type IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { SyncState } from "../../src/sync";
import { defer } from "../../src/utils";
import { logger } from "../../src/logger.ts";
describe("onResumedSync", () => {
let batch: IndexedToDeviceBatch | null;
@@ -35,16 +35,16 @@ describe("onResumedSync", () => {
};
store = new StubStore();
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
store.getOldestToDeviceBatch = vi.fn().mockImplementation(() => {
return batch;
});
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
store.removeToDeviceBatch = vi.fn().mockImplementation(() => {
batch = null;
});
mockClient = getMockClientWithEventEmitter({});
mockClient.store = store;
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
mockClient.sendToDevice = vi.fn().mockImplementation(async () => {
if (shouldFailSendToDevice) {
await Promise.reject(new ConnectionError("")).finally(() => {
setTimeout(onSendToDeviceFailure, 0);
@@ -56,11 +56,11 @@ describe("onResumedSync", () => {
}
});
queue = new ToDeviceMessageQueue(mockClient);
queue = new ToDeviceMessageQueue(mockClient, logger);
});
it("resends queue after connectivity restored", async () => {
const deferred = defer();
const successResolvers = Promise.withResolvers<void>();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
@@ -73,15 +73,15 @@ describe("onResumedSync", () => {
onSendToDeviceSuccess = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
expect(store.removeToDeviceBatch).toHaveBeenCalled();
deferred.resolve();
successResolvers.resolve();
};
queue.start();
return deferred.promise;
return successResolvers.promise;
});
it("does not resend queue if client sync still catching up", async () => {
const deferred = defer();
const successResolvers = Promise.withResolvers<void>();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
@@ -89,15 +89,15 @@ describe("onResumedSync", () => {
resumeSync(SyncState.Catchup, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
deferred.resolve();
successResolvers.resolve();
};
queue.start();
return deferred.promise;
return successResolvers.promise;
});
it("does not resend queue if connectivity restored after queue stopped", async () => {
const deferred = defer();
const successResolvers = Promise.withResolvers<void>();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
@@ -107,10 +107,10 @@ describe("onResumedSync", () => {
resumeSync(SyncState.Syncing, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
deferred.resolve();
successResolvers.resolve();
};
queue.start();
return deferred.promise;
return successResolvers.promise;
});
});
+2 -1
View File
@@ -35,6 +35,7 @@ describe("AutoDiscovery", function () {
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
// eslint-disable-next-line @vitest/expect-expect
it("should throw an error when no domain is specified", function () {
getHttpBackend();
return Promise.all([
@@ -190,7 +191,7 @@ describe("AutoDiscovery", function () {
};
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
AutoDiscovery.findClientConfig("example.org").then((config) => expect(config).toEqual(expected)),
]);
});
+100 -35
View File
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
vi.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(globalThis.Date, "now").mockRestore();
vi.spyOn(globalThis.Date, "now").mockRestore();
});
it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
@@ -183,28 +183,43 @@ describe("Topic content helpers", () => {
it("creates fully defined event content without html", () => {
expect(makeTopicContent("pizza")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "pizza",
mimetype: "text/plain",
},
],
},
});
});
it("creates fully defined event content with html", () => {
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
{
body: "pizza",
mimetype: "text/plain",
},
],
},
});
});
it("creates an empty event when the topic is falsey", () => {
expect(makeTopicContent(undefined)).toEqual({
topic: undefined,
[M_TOPIC.name]: { "m.text": [] },
});
expect(makeTopicContent(null)).toEqual({
topic: null,
[M_TOPIC.name]: { "m.text": [] },
});
});
});
@@ -214,11 +229,13 @@ describe("Topic content helpers", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "pizza",
},
],
},
}),
).toEqual({
text: "pizza",
@@ -229,12 +246,14 @@ describe("Topic content helpers", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "pizza",
mimetype: "text/plain",
},
],
},
}),
).toEqual({
text: "pizza",
@@ -245,17 +264,63 @@ describe("Topic content helpers", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
[M_TOPIC.name]: {
"m.text": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
},
}),
).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
});
it("parses legacy event content", () => {
expect(
parseTopicContent({
topic: "pizza",
}),
).toEqual({
text: "pizza",
});
});
// TODO delete this test and re-enable the next one after support for the invalid form is removed
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
it("parses malformed event content with html topic", () => {
expect(
parseTopicContent({
"topic": "pizza",
"m.topic": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
] as any,
}),
).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
});
it.skip("uses legacy event content when new topic key is invalid", () => {
expect(
parseTopicContent({
"topic": "pizza",
"m.topic": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
] as any,
}),
).toEqual({
text: "pizza",
});
});
});
});
File diff suppressed because it is too large Load Diff
-243
View File
@@ -1,243 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../olm-loader";
import { CrossSigningInfo, createCryptoStoreCacheCallbacks } from "../../../src/crypto/CrossSigning";
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { logger } from "../../../src/logger";
const userId = "@alice:example.com";
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const types = [
{ type: "master", shouldCache: true },
{ type: "self_signing", shouldCache: true },
{ type: "user_signing", shouldCache: true },
{ type: "invalid", shouldCache: false },
];
const badKey = Uint8Array.from(testKey);
badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function () {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function () {
return globalThis.Olm.init();
});
it("should throw if no callback is provided", async () => {
const info = new CrossSigningInfo(userId);
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
});
it.each(types)("should throw if the callback returns falsey", async ({ type, shouldCache }) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => false as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => masterKeyPub as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
});
it("should return a key from its callback", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => testKey,
});
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
// check that the pkSigning object corresponds to the pubKey
const signature = pkSigning.sign("message");
const util = new globalThis.Olm.Utility();
try {
util.ed25519_verify(pubKey, "message", signature);
} finally {
util.free();
}
});
it.each(types)(
"should request a key from the cache callback (if set)" + " and does not call app if one is found" + " %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
if (shouldCache) {
// eslint-disable-next-line jest/no-conditional-expect
expect(getCrossSigningKeyCache).toHaveBeenLastCalledWith(type, expect.any(String));
}
},
);
it.each(types)("should store a key with the cache callback (if set)", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
if (shouldCache) {
// eslint-disable-next-line jest/no-conditional-expect
expect(storeCrossSigningKeyCache).toHaveBeenLastCalledWith(type, testKey);
}
});
it.each(types)("does not store a bad key to the cache", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(new Error("Tried to store a value from cache"));
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
it.each(types)(
"requests a key from the cache callback (if set) and then calls app" + " if one is not found",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
},
);
it.each(types)(
"requests a key from the cache callback (if set) and then" + " calls app if that key doesn't match",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
},
);
});
/*
* Note that MemoryStore is weird. It's only used for testing - as far as I can tell,
* it's not possible to get one in normal execution unless you hack as we do here.
*/
describe.each([
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
[
"MemoryCryptoStore",
() => {
const store = new IndexedDBCryptoStore(undefined!, "tests");
// @ts-ignore set private properties
store._backend = new MemoryCryptoStore();
// @ts-ignore
store._backendPromise = Promise.resolve(store._backend);
return store;
},
],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function (name, dbFactory) {
let store: IndexedDBCryptoStore;
beforeAll(() => {
store = dbFactory();
});
beforeEach(async () => {
await store.deleteAllData();
});
it("should cache data to the store and retrieve it", async () => {
await store.startup();
const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = createCryptoStoreCacheCallbacks(
store,
olmDevice,
);
await storeCrossSigningKeyCache!("self_signing", testKey);
// If we've not saved anything, don't expect anything
// Definitely don't accidentally return the wrong key for the type
const nokey = await getCrossSigningKeyCache!("self", "");
expect(nokey).toBeNull();
const key = await getCrossSigningKeyCache!("self_signing", "");
expect(new Uint8Array(key!)).toEqual(testKey);
});
});

Some files were not shown because too many files have changed in this diff Show More