Compare commits

...

91 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
55 changed files with 3375 additions and 1623 deletions
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature - name: Upload tarball signature
if: ${{ inputs.upload-url }} if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1 uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with: with:
upload_url: ${{ inputs.upload-url }} upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures - name: Upload asset signatures
if: inputs.gpg-fingerprint if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1 uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with: with:
upload_url: ${{ inputs.upload-url }} upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets - name: Upload assets
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1 uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with: with:
upload_url: ${{ inputs.upload-url }} upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }} asset_path: ${{ inputs.asset-path }}
+3 -1
View File
@@ -1,6 +1,8 @@
name: Backport name: Backport
on: 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: types:
- closed - closed
- labeled - labeled
+4 -2
View File
@@ -1,7 +1,9 @@
name: Deploy documentation PR preview name: Deploy documentation PR preview
on: 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"] workflows: ["Static Analysis"]
types: types:
- completed - completed
@@ -15,7 +17,7 @@ jobs:
deployments: write deployments: write
steps: steps:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
@@ -1,7 +1,7 @@
# Triggers after the "Downstream artifacts" build has finished, to run the # 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: on:
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
@@ -21,11 +21,12 @@ concurrency:
jobs: jobs:
playwright: playwright:
name: 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: permissions:
actions: read actions: read
issues: read issues: read
pull-requests: read pull-requests: read
contents: read
with: with:
matrix-js-sdk-sha: ${{ github.sha }} matrix-js-sdk-sha: ${{ github.sha }}
# We only want to run the playwright tests on merge queue to prevent regressions # We only want to run the playwright tests on merge queue to prevent regressions
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it - 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 uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with: with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+8 -5
View File
@@ -1,6 +1,9 @@
name: Pull Request name: Pull Request
on: 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] types: [opened, edited, labeled, unlabeled, synchronize]
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
@@ -15,7 +18,7 @@ jobs:
name: Preview Changelog name: Preview Changelog
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5 - uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
if: github.event_name != 'merge_group' if: github.event_name != 'merge_group'
with: with:
labels: | labels: |
@@ -35,7 +38,7 @@ jobs:
pull-requests: read pull-requests: read
steps: steps:
- name: Add notice - name: Add notice
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked') if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with: with:
script: | script: |
@@ -60,7 +63,7 @@ jobs:
- name: Add label - name: Add label
if: steps.teams.outputs.isTeamMember == 'false' if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with: with:
script: | script: |
github.rest.issues.addLabels({ github.rest.issues.addLabels({
@@ -81,7 +84,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository github.event.pull_request.head.repo.full_name != github.repository
steps: steps:
- name: Close pull request - name: Close pull request
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with: with:
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Check for X-Release-Blocker label on any open issues or PRs - name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
REPO: ${{ inputs.repository }} REPO: ${{ inputs.repository }}
with: with:
@@ -20,9 +20,10 @@ jobs:
with: with:
ref: staging ref: staging
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: package.json node-version-file: package.json
cache: "pnpm" cache: "pnpm"
@@ -49,7 +50,7 @@ jobs:
- name: Ingest upstream changes - name: Ingest upstream changes
if: inputs.include-changes if: inputs.include-changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }} RELEASE_ID: ${{ steps.draft-release.outputs.id }}
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
draft: draft:
permissions: permissions:
contents: write 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]
+3 -2
View File
@@ -27,6 +27,7 @@ jobs:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN # 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 }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
persist-credentials: true
- name: Get actions scripts - name: Get actions scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -37,8 +38,8 @@ jobs:
sparse-checkout: | sparse-checkout: |
scripts/release scripts/release
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
+28 -20
View File
@@ -33,10 +33,14 @@ on:
description: The number of expected assets, including signatures, excluding generated zip & tarball. description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number type: number
required: false required: false
dir: dist-dir:
description: The directory to release description: The directory to release
type: string type: string
default: "." default: "."
version-dirs:
description: Directories in which to update package.json `version` field
type: string
required: false
outputs: outputs:
npm-id: npm-id:
description: "The npm package@version string we published" description: "The npm package@version string we published"
@@ -48,7 +52,7 @@ jobs:
permissions: permissions:
issues: read issues: read
pull-requests: 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: release:
name: Release name: Release
@@ -61,7 +65,7 @@ jobs:
- name: Load GPG key - name: Load GPG key
id: gpg id: gpg
if: inputs.gpg-fingerprint if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6 uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }} passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -69,7 +73,7 @@ jobs:
- name: Get draft release - name: Get draft release
id: draft-release id: draft-release
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1 uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
with: with:
@@ -82,6 +86,7 @@ jobs:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN # 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 }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
persist-credentials: true
- name: Get actions scripts - name: Get actions scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -95,7 +100,7 @@ jobs:
- name: Prepare variables - name: Prepare variables
id: prepare id: prepare
working-directory: ${{ inputs.dir }} working-directory: ${{ inputs.dist-dir }}
run: | run: |
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
@@ -110,7 +115,7 @@ jobs:
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
- name: Check version number not in use - name: Check version number not in use
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with: with:
script: | script: |
const { VERSION } = process.env; const { VERSION } = process.env;
@@ -129,17 +134,17 @@ jobs:
git config --global user.email "releases@riot.im" git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot" git config --global user.name "RiotRobot"
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: ${{ inputs.dir }}/package.json node-version-file: ${{ inputs.dist-dir }}/package.json
- name: Install dependencies - name: Install dependencies
run: "pnpm install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- name: Handle develop dependencies - name: Handle develop dependencies
working-directory: ${{ inputs.dir }} working-directory: ${{ inputs.dist-dir }}
run: | run: |
ret=0 ret=0
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
@@ -153,11 +158,14 @@ jobs:
git commit -m "Keep $PACKAGE at $VERSION" git commit -m "Keep $PACKAGE at $VERSION"
done done
- name: Bump package.json version - name: Bump package.json versions
working-directory: ${{ inputs.dir }}
run: | run: |
pnpm version --no-git-tag-version "${VERSION#v}" for DIR in $DIRS; do
git add package.json 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 - name: Add to CHANGELOG.md
if: inputs.final if: inputs.final
@@ -184,7 +192,7 @@ jobs:
- name: Build assets - name: Build assets
if: steps.prepare.outputs.has-dist-script == '1' if: steps.prepare.outputs.has-dist-script == '1'
working-directory: ${{ inputs.dir }} working-directory: ${{ inputs.dist-dir }}
run: DIST_VERSION="$VERSION" pnpm dist run: DIST_VERSION="$VERSION" pnpm dist
- name: Upload release assets & signatures - name: Upload release assets & signatures
@@ -193,7 +201,7 @@ jobs:
with: with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }} gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.draft-release.outputs.upload_url }} upload-url: ${{ steps.draft-release.outputs.upload_url }}
asset-path: ${{ inputs.dir }}/${{ inputs.asset-path }} asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
- name: Create signed tag - name: Create signed tag
if: inputs.gpg-fingerprint if: inputs.gpg-fingerprint
@@ -226,7 +234,7 @@ jobs:
- name: Validate release has expected assets - name: Validate release has expected assets
if: inputs.expected-asset-count if: inputs.expected-asset-count
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }} RELEASE_ID: ${{ steps.draft-release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }} EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
@@ -254,7 +262,7 @@ jobs:
git push origin master git push origin master
- name: Publish release - name: Publish release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }} RELEASE_ID: ${{ steps.draft-release.outputs.id }}
FINAL: ${{ inputs.final }} FINAL: ${{ inputs.final }}
@@ -286,9 +294,9 @@ jobs:
name: Publish to npm name: Publish to npm
needs: release needs: release
if: inputs.npm if: inputs.npm
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
with: with:
dir: ${{ inputs.dir }} dir: ${{ inputs.dist-dir }}
permissions: permissions:
contents: read contents: read
id-token: write id-token: write
+3 -2
View File
@@ -25,10 +25,11 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: staging ref: staging
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache - name: 🔧 pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
+10 -7
View File
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
permissions: {} # No permissions required permissions: {} # No permissions required
jobs: jobs:
release: 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: permissions:
contents: write contents: write
issues: write issues: write
@@ -50,9 +50,10 @@ jobs:
repository: ${{ matrix.repo }} repository: ${{ matrix.repo }}
ref: staging ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
persist-credentials: true
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version: "lts/*" node-version: "lts/*"
@@ -77,10 +78,12 @@ jobs:
steps: steps:
- name: 🧮 Checkout code - name: 🧮 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache - name: 🔧 pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
@@ -92,7 +95,7 @@ jobs:
run: pnpm gendoc run: pnpm gendoc
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with: with:
path: _docs path: _docs
@@ -110,4 +113,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
+9 -5
View File
@@ -13,6 +13,10 @@ on:
type: boolean type: boolean
required: false required: false
description: "Whether to combine multiple LCOV and 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: {} permissions: {}
jobs: jobs:
sonarqube: sonarqube:
@@ -41,9 +45,10 @@ jobs:
repository: ${{ github.event.workflow_run.head_repository.full_name }} repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow 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 fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
persist-credentials: false
- name: 📥 Download artifact - name: 📥 Download artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: ${{ !inputs.sharded }} if: ${{ !inputs.sharded }}
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,14 +56,13 @@ jobs:
name: coverage name: coverage
path: coverage path: coverage
- name: 📥 Download sharded artifacts - name: 📥 Download sharded artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: inputs.sharded if: inputs.sharded
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
pattern: coverage-* pattern: coverage-*
path: coverage path: coverage
merge-multiple: true
- name: Check coverage artifact - name: Check coverage artifact
run: | run: |
if [ ! -d coverage ]; then if [ ! -d coverage ]; then
@@ -75,7 +79,7 @@ jobs:
- name: "🩻 SonarCloud Scan" - name: "🩻 SonarCloud Scan"
id: sonarcloud id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@ea0cd9dbd5562e79816685972bc0d03c235a900c uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
# workflow_run fails report against the develop commit always, we don't want that for PRs # 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' }} continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with: with:
@@ -83,7 +87,7 @@ jobs:
repository: ${{ github.event.workflow_run.head_repository.full_name }} repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }} is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
skip_coverage_label: Z-Skip-Coverage skip_coverage_label: Z-Skip-Coverage
version_cmd: "cat package.json | jq -r .version" version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }} branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }} revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }} token: ${{ secrets.SONAR_TOKEN }}
+4 -2
View File
@@ -1,6 +1,8 @@
name: SonarQube name: SonarQube
on: 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"] workflows: ["Tests"]
types: types:
- completed - completed
@@ -16,7 +18,7 @@ jobs:
actions: read actions: read
statuses: write statuses: write
id-token: write # sonar 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: secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+45 -21
View File
@@ -15,9 +15,11 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
@@ -33,9 +35,11 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
@@ -51,9 +55,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
@@ -79,11 +85,15 @@ jobs:
workflow_lint: workflow_lint:
name: "Workflow Lint" name: "Workflow Lint"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
security-events: write
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
@@ -94,14 +104,19 @@ jobs:
- name: Run Linter - name: Run Linter
run: "pnpm lint:workflows" run: "pnpm lint:workflows"
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
docs: docs:
name: "JSDoc Checker" name: "JSDoc Checker"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
@@ -113,7 +128,7 @@ jobs:
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles" run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: docs name: docs
path: _docs path: _docs
@@ -125,9 +140,11 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
@@ -146,9 +163,10 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
repository: element-hq/element-web repository: element-hq/element-web
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version: "lts/*" node-version: "lts/*"
@@ -163,13 +181,19 @@ jobs:
working-directory: apps/web working-directory: apps/web
run: "pnpm run lint:types" run: "pnpm run lint:types"
# Hook for branch protection to skip downstream typechecking outside of merge queues # Workflow consolidation job
downstream: done:
name: Downstream Typescript Syntax Check needs:
- ts_lint
- js_lint
- node_example_lint
- workflow_lint
- docs
- analyse_dead_code
- element-web
name: Static Analysis
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
if: always() if: always()
needs:
- element-web
steps: steps:
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success' - if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1 run: exit 1
+1 -1
View File
@@ -11,7 +11,7 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
sync-labels: 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: with:
LABELS: | LABELS: |
element-hq/element-meta element-hq/element-meta
+14 -9
View File
@@ -23,11 +23,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node - name: Setup Node
id: setupNode id: setupNode
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "pnpm" cache: "pnpm"
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
@@ -42,19 +44,22 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
pnpm test \ pnpm test \
--coverage=${{ env.ENABLE_COVERAGE }} \ --coverage=${ENABLE_COVERAGE} \
--maxWorkers ${{ steps.cpu-cores.outputs.count }} \ --maxWorkers ${NUM_WORKERS} \
./spec/${{ matrix.specs }} ./spec/${{ matrix.specs }}
env: env:
SHARD: ${{ matrix.specs }} SHARD: ${{ matrix.specs }}
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
- name: Move coverage files into place - name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true' 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 - name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true' if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }} name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
path: | path: |
@@ -74,7 +79,7 @@ jobs:
element-web: element-web:
name: Downstream test element-web name: Downstream test element-web
if: github.event_name == 'merge_group' 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: permissions:
statuses: write statuses: write
with: with:
@@ -84,8 +89,8 @@ jobs:
complement-crypto: complement-crypto:
name: "Run Complement Crypto tests" name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group' if: github.event_name == 'merge_group'
permissions: read-all permissions: read-all # zizmor: ignore[excessive-permissions]
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
with: with:
use_js_sdk: "." use_js_sdk: "."
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
automate-project-columns-next: automate-project-columns-next:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/add-to-project@main - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with: with:
project-url: https://github.com/orgs/element-hq/projects/120 project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -6,6 +6,6 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
call-triage-labelled: 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: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+24
View File
@@ -1,3 +1,27 @@
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) Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
================================================================================================== ==================================================================================================
## 🚨 BREAKING CHANGES ## 🚨 BREAKING CHANGES
+3 -4
View File
@@ -1,5 +1,8 @@
import { KnipConfig } from "knip"; 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 { export default {
entry: [ entry: [
"src/index.ts", "src/index.ts",
@@ -28,10 +31,6 @@ export default {
"husky", "husky",
// Used in script which only runs in environment with `@octokit/rest` installed // Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest", "@octokit/rest",
// Used by `vitest`
"vitest-sonar-reporter",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
], ],
ignoreBinaries: [ ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml` // Used when available by reusable workflow `.github/workflows/release-make.yml`
+17 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "41.1.0-rc.0", "version": "41.3.0",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"engines": { "engines": {
"node": ">=22.0.0" "node": ">=22.0.0"
@@ -18,8 +18,8 @@
"lint:types": "tsc --noEmit", "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:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip", "lint:knip": "knip",
"test": "vitest", "test": "vitest run",
"test:watch": "vitest --watch", "test:watch": "vitest watch",
"coverage": "pnpm test --coverage" "coverage": "pnpm test --coverage"
}, },
"repository": { "repository": {
@@ -48,7 +48,7 @@
], ],
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^17.1.0", "@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
"another-json": "^0.2.0", "another-json": "^0.2.0",
"bs58": "^6.0.0", "bs58": "^6.0.0",
"content-type": "^1.0.4", "content-type": "^1.0.4",
@@ -57,10 +57,9 @@
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.16.1", "matrix-widget-api": "^1.16.1",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "^3.0.1",
"p-retry": "7", "p-retry": "8",
"sdp-transform": "^3.0.0", "sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6", "unhomoglyph": "^1.0.6"
"uuid": "13"
}, },
"devDependencies": { "devDependencies": {
"@action-validator/cli": "^0.6.0", "@action-validator/cli": "^0.6.0",
@@ -106,21 +105,18 @@
"fetch-mock": "^12.6.0", "fetch-mock": "^12.6.0",
"happy-dom": "^20.1.0", "happy-dom": "^20.1.0",
"husky": "^9.0.0", "husky": "^9.0.0",
"knip": "^5.0.0", "knip": "^6.0.0",
"lint-staged": "^16.0.0", "lint-staged": "^16.0.0",
"matrix-mock-request": "^2.5.0", "matrix-mock-request": "^2.5.0",
"prettier": "3.8.1", "prettier": "3.8.3",
"typedoc": "^0.28.1", "typedoc": "^0.28.1",
"typedoc-plugin-coverage": "^4.0.0", "typedoc-plugin-coverage": "^4.0.0",
"typedoc-plugin-mdn-links": "^5.0.0", "typedoc-plugin-mdn-links": "^5.0.0",
"typedoc-plugin-missing-exports": "^4.0.0", "typedoc-plugin-missing-exports": "^4.0.0",
"typescript": "^5.4.2", "typescript": "^6.0.0",
"vitest": "^4.0.17", "vitest": "^4.0.17",
"vitest-sonar-reporter": "^3.0.0" "vitest-sonar-reporter": "^3.0.0"
}, },
"resolutions": {
"expect": "30.2.0"
},
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"allowedVersions": { "allowedVersions": {
@@ -129,7 +125,14 @@
}, },
"allowedDeprecatedVersions": { "allowedDeprecatedVersions": {
"eslint": "8" "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.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc" "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
} }
+1597 -1225
View File
File diff suppressed because it is too large Load Diff
+110 -2
View File
@@ -86,6 +86,7 @@ import {
encryptGroupSessionKey, encryptGroupSessionKey,
encryptMegolmEvent, encryptMegolmEvent,
encryptMegolmEventRawPlainText, encryptMegolmEventRawPlainText,
encryptOlmEvent,
establishOlmSession, establishOlmSession,
getTestOlmAccountKeys, getTestOlmAccountKeys,
expectSendRoomKey, expectSendRoomKey,
@@ -2064,6 +2065,7 @@ describe("crypto", () => {
expect(hasCrossSigningKeysForUser).toBe(true); expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID); const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.known).toBe(false); // We haven't actually stashed a copy of Alice's identity
expect(verificationStatus.isVerified()).toBe(false); expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2078,7 +2080,8 @@ describe("crypto", () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID); const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true); expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID); const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
expect(verificationStatus.known).toBe(true);
expect(verificationStatus.isVerified()).toBe(false); expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2089,7 +2092,8 @@ describe("crypto", () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz"); const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false); expect(hasCrossSigningKeysForUser).toBe(false);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID); const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus("@unknown:xyz");
expect(verificationStatus.known).toBe(false);
expect(verificationStatus.isVerified()).toBe(false); expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2119,6 +2123,7 @@ describe("crypto", () => {
{ {
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID); const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.known).toBe(true);
expect(verificationStatus.isVerified()).toBe(false); expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2311,4 +2316,107 @@ describe("crypto", () => {
); );
} }
}); });
describe("secret pushing", () => {
it("should push a new backup key when a new backup key is set", async () => {
// setup: alice has another device, DEVICE_ID, which is verified
const crypto = aliceClient.getCrypto()!;
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
await startClientAndAwaitFirstSync();
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
// when we set a new backup key
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
fetchMock.post("path:/_matrix/client/v3/room_keys/version", {
status: 200,
body: { version: "1" },
});
const secretPushPromise = new Promise<any>((resolve) => {
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return {};
});
});
await crypto.resetKeyBackup();
// we expect the other device to get a secret push
const content = await secretPushPromise;
const curve25519key = JSON.parse(testOlmAccount.identity_keys()).curve25519;
const ciphertext = content.messages["@alice:localhost"].DEVICE_ID.ciphertext[curve25519key];
const olmSession = new Olm.Session();
olmSession.create_inbound(testOlmAccount, ciphertext.body);
const decrypted = JSON.parse(olmSession.decrypt(0, ciphertext.body));
expect(decrypted.type).toBe("io.element.msc4385.secret.push");
expect(decrypted.content.name).toBe("m.megolm_backup.v1");
});
it("should receive pushed backup key", async () => {
// setup: alice has another device, DEVICE_ID, which is verified,
// and has a key backup set up and signed by DEVICE_ID
const crypto = aliceClient.getCrypto()!;
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
await startClientAndAwaitFirstSync();
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
// after we push the backup key to alice...
const senderIdentityKeys = JSON.parse(testOlmAccount.identity_keys());
const aliceDeviceKeys = await crypto.getOwnDeviceKeys();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const secretPush = encryptOlmEvent({
sender: "@alice:localhost",
senderKey: senderIdentityKeys.curve25519,
senderSigningKey: senderIdentityKeys.ed25519,
p2pSession,
recipient: "@alice:localhost",
recipientCurve25519Key: aliceDeviceKeys.curve25519,
recipientEd25519Key: aliceDeviceKeys.ed25519,
plaincontent: {
secret: testData.BACKUP_DECRYPTION_KEY_BASE64,
name: "m.megolm_backup.v1",
},
plaintype: "io.element.msc4385.secret.push",
});
const syncResponse = {
next_batch: 1,
to_device: {
events: [secretPush],
},
};
const backupKeyReceivedPromise = new Promise<string>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupDecryptionKeyCached, resolve);
});
const keyBackupEnabledPromise = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
// alice should be using backup now
expect(await backupKeyReceivedPromise).toBe(testData.SIGNED_BACKUP_DATA.version);
await keyBackupEnabledPromise;
expect(await crypto.getActiveSessionBackupVersion()).toBe(testData.SIGNED_BACKUP_DATA.version);
});
});
}); });
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm"; import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "@fetch-mock/vitest"; import fetchMock from "@fetch-mock/vitest";
import { import {
@@ -146,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
const ourChannel = new MSC4108SecureChannel(ourMockSession); const ourChannel = new MSC4108SecureChannel(ourMockSession);
const qrCodeData = QrCodeData.fromBytes( 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); const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
@@ -279,7 +279,7 @@ 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}}` */ /** 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) }; 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}}`. * 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. * Contains the key from {prefix}MEGOLM_SESSION_DATA.
*/ */
@@ -422,7 +422,7 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)), "ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
}, },
"forwarding_curve25519_key_chain": [], "forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": True, "m.shared_history": True,
} }
return megolm_export, private_key return megolm_export, private_key
+25
View File
@@ -3940,6 +3940,31 @@ describe("MatrixClient", function () {
}); });
expect(httpLookups.length).toEqual(0); expect(httpLookups.length).toEqual(0);
}); });
it("should handle no jwks_uri", async () => {
const { jwks_uri: _, ...metadata } = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
{
method: "GET",
path: `/auth_issuer`,
data: { issuer: metadata.issuer },
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: null,
});
expect(httpLookups.length).toEqual(0);
});
}); });
describe("identityHashedLookup", () => { describe("identityHashedLookup", () => {
+1 -1
View File
@@ -427,7 +427,7 @@ describe("CallMembership", () => {
}); });
it("uses unpadded base64 for RTC backend identities", async () => { it("uses unpadded base64 for RTC backend identities", async () => {
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate })); const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
expect(membership.rtcBackendIdentity).toBe("j9N1u04ZbvI9qKf3cxrf2NauD-fIGJ4uAcYkfI9V7SY"); expect(membership.rtcBackendIdentity).toBe("jUZ0Q1yF5nV3LlAI5xfD1I7BPnAytJaPEAR57EXjJ6s");
}); });
}); });
}); });
@@ -0,0 +1,27 @@
/*
Copyright 2026 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 { computeRtcIdentityRaw } from "../../../src/matrixrtc/membershipData/index.ts";
describe("computeRtcIdentityRaw", () => {
it("should compute the correct identity hash", async () => {
// Test vector taken from the spec, with the expected output updated to match the unpadded base64 encoding
// https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md#appendix-hash-derivation-test-vectors
const result = await computeRtcIdentityRaw("@alice:example.com", "DEVICE123", "memberABC");
// Add assertions based on expected hash output
expect(result).toBe("J+T45tGruxc+HrUOqJJlyQSV33m728Cme4+vt8/SWrU");
});
});
+44 -1
View File
@@ -175,12 +175,29 @@ describe("oidc authorization", () => {
expect(authUrl.searchParams.get("login_hint")).toEqual("login1234"); expect(authUrl.searchParams.get("login_hint")).toEqual("login1234");
}); });
it("should generate url with response_mode=fragment", async () => {
const nonce = "abc123";
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
nonce,
responseMode: "fragment",
}),
);
expect(authUrl.searchParams.get("response_mode")).toEqual("fragment");
});
}); });
describe("completeAuthorizationCodeGrant", () => { describe("completeAuthorizationCodeGrant", () => {
const homeserverUrl = "https://server.org/"; const homeserverUrl = "https://server.org/";
const identityServerUrl = "https://id.org/"; const identityServerUrl = "https://id.org/";
const nonce = "test-nonce"; const nonce = "hRpB6pkE06";
const redirectUri = baseUrl; const redirectUri = baseUrl;
const code = "auth_code_xyz"; const code = "auth_code_xyz";
const validBearerTokenResponse = { const validBearerTokenResponse = {
@@ -290,6 +307,32 @@ describe("oidc authorization", () => {
expect(queryParams.get("code")).toEqual(code); expect(queryParams.get("code")).toEqual(code);
}); });
it("should make correct request to the token endpoint with response_mode=fragment", async () => {
const state = await setupState({ responseMode: "fragment" });
const codeVerifier = getValueFromStorage(state, "code_verifier");
await completeAuthorizationCodeGrant(code, state, "fragment");
expect(fetchMock.callHistory.lastCall(metadata.token_endpoint)?.options).toStrictEqual(
expect.objectContaining({
method: "post",
credentials: "same-origin",
headers: {
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded",
},
}),
);
// check body is correctly formed
const queryParams = fetchMock.callHistory.lastCall(metadata.token_endpoint)!.options
.body as URLSearchParams;
expect(queryParams.get("grant_type")).toEqual("authorization_code");
expect(queryParams.get("client_id")).toEqual(clientId);
expect(queryParams.get("code_verifier")).toEqual(codeVerifier);
expect(queryParams.get("redirect_uri")).toEqual(redirectUri);
expect(queryParams.get("code")).toEqual(code);
});
it("should return with valid bearer token", async () => { it("should return with valid bearer token", async () => {
const state = await setupState(); const state = await setupState();
const scope = getValueFromStorage(state, "scope"); const scope = getValueFromStorage(state, "scope");
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type EstablishedEcies, QrCodeData, QrCodeMode, Ecies } from "@matrix-org/matrix-sdk-crypto-wasm"; import { type EstablishedEcies, QrCodeData, QrCodeIntent, Ecies } from "@matrix-org/matrix-sdk-crypto-wasm";
import { MSC4108RendezvousSession, MSC4108SecureChannel, PayloadType } from "../../../../src/rendezvous"; import { MSC4108RendezvousSession, MSC4108SecureChannel, PayloadType } from "../../../../src/rendezvous";
@@ -28,7 +28,7 @@ describe("MSC4108SecureChannel", () => {
}); });
const channel = new MSC4108SecureChannel(session); const channel = new MSC4108SecureChannel(session);
const code = await channel.generateCode(QrCodeMode.Login); const code = await channel.generateCode(QrCodeIntent.Login);
expect(code).toHaveLength(71); expect(code).toHaveLength(71);
const text = new TextDecoder().decode(code); const text = new TextDecoder().decode(code);
expect(text.startsWith("MATRIX")).toBeTruthy(); expect(text.startsWith("MATRIX")).toBeTruthy();
@@ -43,7 +43,7 @@ describe("MSC4108SecureChannel", () => {
} as unknown as MSC4108RendezvousSession; } as unknown as MSC4108RendezvousSession;
const channel = new MSC4108SecureChannel(mockSession); const channel = new MSC4108SecureChannel(mockSession);
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl)); const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel( const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
qrCodeData.publicKey, qrCodeData.publicKey,
"MATRIX_QR_CODE_LOGIN_INITIATE", "MATRIX_QR_CODE_LOGIN_INITIATE",
@@ -64,7 +64,7 @@ describe("MSC4108SecureChannel", () => {
vi.mocked(mockSession.receive).mockResolvedValue(""); vi.mocked(mockSession.receive).mockResolvedValue("");
await expect(channel.connect()).rejects.toThrow("No response from other device"); await expect(channel.connect()).rejects.toThrow("No response from other device");
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl)); const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel( const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
qrCodeData.publicKey, qrCodeData.publicKey,
"NOT_REAL_MATRIX_QR_CODE_LOGIN_INITIATE", "NOT_REAL_MATRIX_QR_CODE_LOGIN_INITIATE",
@@ -87,7 +87,7 @@ describe("MSC4108SecureChannel", () => {
} as unknown as MSC4108RendezvousSession; } as unknown as MSC4108RendezvousSession;
channel = new MSC4108SecureChannel(mockSession); channel = new MSC4108SecureChannel(mockSession);
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl)); const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
const { channel: _opponentChannel, initial_message: ciphertext } = new Ecies().establish_outbound_channel( const { channel: _opponentChannel, initial_message: ciphertext } = new Ecies().establish_outbound_channel(
qrCodeData.publicKey, qrCodeData.publicKey,
"MATRIX_QR_CODE_LOGIN_INITIATE", "MATRIX_QR_CODE_LOGIN_INITIATE",
+79 -1
View File
@@ -21,6 +21,7 @@ import {
KeysQueryRequest, KeysQueryRequest,
Migration, Migration,
OlmMachine, OlmMachine,
type OtherUserIdentity,
type PickledInboundGroupSession, type PickledInboundGroupSession,
type PickledSession, type PickledSession,
StoreHandle, StoreHandle,
@@ -109,6 +110,7 @@ describe("initRustCrypto", () => {
getBackupKeys: vi.fn(), getBackupKeys: vi.fn(),
getIdentity: vi.fn().mockResolvedValue(null), getIdentity: vi.fn().mockResolvedValue(null),
trackedUsers: vi.fn(), trackedUsers: vi.fn(),
getAllRoomsPendingKeyBundles: vi.fn().mockResolvedValue([]),
} as unknown as Mocked<OlmMachine>; } as unknown as Mocked<OlmMachine>;
} }
@@ -590,6 +592,72 @@ describe("RustCrypto", () => {
expect(res.length).toEqual(0); expect(res.length).toEqual(0);
}); });
it.each(["m.room_key_bundle", "io.element.msc4268.room_key_bundle"])(
"should accept key bundles when we find out about them",
async (type: string) => {
// Given we are faking that the received to-device message is a
// decrypted room key bundle.
// @ts-ignore Overriding a private function
rustCrypto.receiveSyncChanges = vi.fn().mockReturnValue([keyBundleEvent(type)]);
// And that there is a pending key bundle
// @ts-ignore Overriding a private function
rustCrypto.olmMachine.getPendingKeyBundleDetailsForRoom = vi.fn().mockReturnValue({
inviteAcceptedAtMillis: Date.now(),
inviterId: { toString: vi.fn().mockReturnValue("@inv:s.co") },
});
// When we process to-device messages
rustCrypto.maybeAcceptKeyBundle = vi.fn().mockName("maybeAcceptKeyBundle").mockResolvedValue(null);
await rustCrypto.preprocessToDeviceMessages([]);
// Then we accepted the key bundle
expect(rustCrypto.maybeAcceptKeyBundle).toHaveBeenCalledWith("!r:s.co", "@inv:s.co");
},
);
it("should not accept other to-device messages as key bundles when we receive them", async () => {
// Given we are faking that the received to-device message looks
// like a room key bundle, except it has the wrong type.
// @ts-ignore Overriding a private function
rustCrypto.receiveSyncChanges = vi.fn().mockReturnValue([keyBundleEvent("foo.some_other_type")]);
// And that there is a pending key bundle
// @ts-ignore Overriding a private function
rustCrypto.olmMachine.getPendingKeyBundleDetailsForRoom = vi.fn().mockReturnValue({
inviteAcceptedAtMillis: Date.now(),
inviterId: { toString: vi.fn().mockReturnValue("@inv:s.co") },
});
// When we process to-device messages
rustCrypto.maybeAcceptKeyBundle = vi.fn().mockName("maybeAcceptKeyBundle").mockResolvedValue(null);
await rustCrypto.preprocessToDeviceMessages([]);
// Then we do not try to accepted a key bundle
expect(rustCrypto.maybeAcceptKeyBundle).not.toHaveBeenCalledWith();
});
function keyBundleEvent(type: string): RustSdkCryptoJs.ProcessedToDeviceEvent {
return {
rawEvent: JSON.stringify({
content: { room_id: "!r:s.co" },
sender: "",
type,
}),
type: 0,
encryptionInfo: {
sender: "",
senderDevice: null,
senderCurve25519Key: "",
isSenderVerified: vi.fn().mockReturnValue(true),
},
} as any as RustSdkCryptoJs.ProcessedToDeviceEvent;
}
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => { it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
rustCrypto = await makeTestRustCrypto( rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), { new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
@@ -788,6 +856,7 @@ describe("RustCrypto", () => {
undefined, undefined,
secretStorage, secretStorage,
); );
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
async function createSecretStorageKey() { async function createSecretStorageKey() {
return { return {
@@ -835,6 +904,7 @@ describe("RustCrypto", () => {
{} as CryptoCallbacks, {} as CryptoCallbacks,
false, false,
); );
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
async function createSecretStorageKey() { async function createSecretStorageKey() {
return { return {
@@ -1524,6 +1594,7 @@ describe("RustCrypto", () => {
it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => { it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => {
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID); const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.known).toBe(false);
expect(userVerificationStatus.isVerified()).toBeFalsy(); expect(userVerificationStatus.isVerified()).toBeFalsy();
expect(userVerificationStatus.isTofu()).toBeFalsy(); expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy(); expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy();
@@ -1535,9 +1606,10 @@ describe("RustCrypto", () => {
free: vi.fn(), free: vi.fn(),
isVerified: vi.fn().mockReturnValue(true), isVerified: vi.fn().mockReturnValue(true),
wasPreviouslyVerified: vi.fn().mockReturnValue(true), wasPreviouslyVerified: vi.fn().mockReturnValue(true),
}); } as unknown as OtherUserIdentity);
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID); const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.known).toBe(true);
expect(userVerificationStatus.isVerified()).toBeTruthy(); expect(userVerificationStatus.isVerified()).toBeTruthy();
expect(userVerificationStatus.isTofu()).toBeFalsy(); expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy(); expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
@@ -2320,6 +2392,7 @@ describe("RustCrypto", () => {
}); });
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage); const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
// We have a key backup // We have a key backup
await waitFor(async () => expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull()); await waitFor(async () => expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull());
@@ -2388,6 +2461,7 @@ describe("RustCrypto", () => {
queryKeysForUsers: vi.fn().mockReturnValue({}), queryKeysForUsers: vi.fn().mockReturnValue({}),
getReceivedRoomKeyBundleData: vi.fn(), getReceivedRoomKeyBundleData: vi.fn(),
receiveRoomKeyBundle: vi.fn(), receiveRoomKeyBundle: vi.fn(),
clearRoomPendingKeyBundle: vi.fn(),
} as unknown as Mocked<OlmMachine>; } as unknown as Mocked<OlmMachine>;
const http = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), { const http = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
@@ -2448,6 +2522,10 @@ describe("RustCrypto", () => {
expect(mockOlmMachine.receiveRoomKeyBundle).toHaveBeenCalledTimes(1); expect(mockOlmMachine.receiveRoomKeyBundle).toHaveBeenCalledTimes(1);
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][0]).toBe(bundleData); expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][0]).toBe(bundleData);
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl")); expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl"));
// It should also flag the room as not waiting for a key bundle
expect(mockOlmMachine.clearRoomPendingKeyBundle).toHaveBeenCalledTimes(1);
expect(mockOlmMachine.clearRoomPendingKeyBundle.mock.calls[0][0].toString()).toEqual("!room_id");
}); });
}); });
+10 -4
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/ */
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { type OutgoingRequest } from "@matrix-org/matrix-sdk-crypto-wasm"; import { type OtherUserIdentity, type OutgoingRequest } from "@matrix-org/matrix-sdk-crypto-wasm";
import { type Mocked } from "vitest"; import { type Mocked } from "vitest";
import { import {
@@ -138,7 +138,9 @@ describe("VerificationRequest", () => {
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]); await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification // Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId)); const bobUserIdentity = (await aliceOlmMachine.getIdentity(
new RustSdkCryptoJs.UserId(bobUserId),
)) as OtherUserIdentity;
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org"); const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")]; const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
@@ -276,7 +278,9 @@ describe("VerificationRequest", () => {
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]); await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification // Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId)); const bobUserIdentity = (await aliceOlmMachine.getIdentity(
new RustSdkCryptoJs.UserId(bobUserId),
)) as OtherUserIdentity;
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org"); const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")]; const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
@@ -394,7 +398,9 @@ describe("VerificationRequest", () => {
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]); await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification // Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId)); const bobUserIdentity = (await aliceOlmMachine.getIdentity(
new RustSdkCryptoJs.UserId(bobUserId),
)) as OtherUserIdentity;
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org"); const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [ const methods = [
+35
View File
@@ -79,6 +79,41 @@ describe("IndexedDBStore", () => {
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2); expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
}); });
it("should handle failed queries", async () => {
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
});
await store.startup();
// Simulate a failed query
let txn: IDBRequest;
(store.backend as LocalIndexedDBStoreBackend)["db"]!.transaction = (): IDBTransaction => {
return {
objectStore: (name: string) =>
({
name,
openCursor: (query: unknown) => {
return (txn = {
error: new DOMException("Expected error"),
} as IDBRequest);
},
}) as IDBObjectStore,
} as IDBTransaction;
};
// Call backend directly as otherwise the error is masked.
const promise = store.backend.getClientOptions();
// The function uses a Promise.then(() => trick to delay execution
// so we need to wait before we can call the txn onerror handler.
process.nextTick(() => {
txn!.onerror!(new Event("we-ignore-this"));
});
await expect(() => promise).rejects.toThrow("selectQuery failed for client_options");
});
it("Should load presence events on startup", async () => { it("Should load presence events on startup", async () => {
// 1. Create idb database // 1. Create idb database
const indexedDB = new IDBFactory(); const indexedDB = new IDBFactory();
+14 -2
View File
@@ -416,9 +416,12 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[EventType.Direct]: { [userId: string]: string[] }; [EventType.Direct]: { [userId: string]: string[] };
[EventType.IgnoredUserList]: { ignored_users: { [userId: string]: EmptyObject } }; [EventType.IgnoredUserList]: { ignored_users: { [userId: string]: EmptyObject } };
"m.secret_storage.default_key": { key: string }; "m.secret_storage.default_key": { key: string };
// Flag set by the rust SDK (Element X) and also used by us to mark that the user opted out of backup
// (I don't know why it's m.org.matrix...) // MSC4287: Sharing key backup preference between clients - used to mark that the user opted out of key storage
"m.key_backup": { enabled: boolean };
// MSC4287 unstable prefix (note the boolean property has the opposite sense)
"m.org.matrix.custom.backup_disabled": { disabled: boolean }; "m.org.matrix.custom.backup_disabled": { disabled: boolean };
"m.identity_server": { base_url: string | null }; "m.identity_server": { base_url: string | null };
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings; [key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription; [key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
@@ -428,6 +431,15 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any }; [POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
[EventType.InvitePermissionConfig]: { default_action?: string }; [EventType.InvitePermissionConfig]: { default_action?: string };
// List of recently used reaction emojis
// https://spec.matrix.org/v1.18/client-server-api/#mrecent_emoji
"m.recent_emoji": {
recent_emoji: Array<{
emoji: string;
total: number;
}>;
};
} }
/** /**
+3 -1
View File
@@ -58,7 +58,9 @@ export interface InviteOpts {
/** /**
* Before sending the invite, if the room is encrypted, share the keys for any messages sent while the history * Before sending the invite, if the room is encrypted, share the keys for any messages sent while the history
* visibility was `shared`, via the experimental * visibility was `shared`, via the experimental
* support for [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268). * support for [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268). If the room's current
* history visibility setting is neither `shared` nor `world_readable`, history sharing will be disabled to prevent
* exposing keys for messages sent prior to the visibility restriction.
* *
* @experimental * @experimental
*/ */
+12 -7
View File
@@ -101,7 +101,7 @@ import {
type RoomNameState, type RoomNameState,
} from "./models/room.ts"; } from "./models/room.ts";
import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-member.ts"; import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-member.ts";
import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts"; import { RoomStateEvent, type IPowerLevelsContent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
import { import {
isSendDelayedEventRequestOpts, isSendDelayedEventRequestOpts,
UpdateDelayedEventAction, UpdateDelayedEventAction,
@@ -2027,6 +2027,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// attach the event listeners needed by RustCrypto // attach the event listeners needed by RustCrypto
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto)); this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
this.on(RoomStateEvent.Events, rustCrypto.onRoomStateEvent.bind(rustCrypto));
this.on(ClientEvent.Event, (event) => { this.on(ClientEvent.Event, (event) => {
rustCrypto.onLiveEventFromSync(event); rustCrypto.onLiveEventFromSync(event);
}); });
@@ -2428,12 +2429,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const roomId = res.room_id; const roomId = res.room_id;
if (opts.acceptSharedHistory && inviter && this.cryptoBackend) { if (opts.acceptSharedHistory && inviter && this.cryptoBackend) {
// Flag upfront that we are waiting for a key bundle, so that if we crash mid-import, we can try again.
await this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
// Try to accept the room key bundle specified in a `m.room_key_bundle` to-device message we (might have) already received. // Try to accept the room key bundle specified in a `m.room_key_bundle` to-device message we (might have) already received.
const bundleDownloaded = await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter); await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter);
// If this fails, i.e. we haven't received this message yet, we need to wait until the to-device message arrives.
if (!bundleDownloaded) {
this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
}
} }
// In case we were originally given an alias, check the room cache again // In case we were originally given an alias, check the room cache again
@@ -4088,7 +4087,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
if (opts.shareEncryptedHistory) { if (opts.shareEncryptedHistory) {
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId); const historyVisibility = this.getRoom(roomId)?.getHistoryVisibility() ?? HistoryVisibility.Shared;
// We should only share room history if the *current* visibility allows it.
if ([HistoryVisibility.Invited, HistoryVisibility.Joined].includes(historyVisibility)) {
this.logger.debug("Not sharing message history as the room history visibility is currently unshared");
} else {
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId);
}
} }
return await this.membershipChange(roomId, userId, KnownMembership.Invite, opts.reason); return await this.membershipChange(roomId, userId, KnownMembership.Invite, opts.reason);
+13 -1
View File
@@ -80,6 +80,18 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
*/ */
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>; importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Room key history sharing (MSC4268)
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Share any shareable E2EE history in the given room with the given recipient,
* as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
*/
shareRoomHistoryWithUser(roomId: string, userId: string): Promise<void>;
/** /**
* Having accepted an invite for the given room from the given user, attempt to * Having accepted an invite for the given room from the given user, attempt to
* find information about a room key bundle and, if found, download the * find information about a room key bundle and, if found, download the
@@ -103,7 +115,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @param roomId - The room we were invited to, for which we did not receive a key bundle before accepting the invite. * @param roomId - The room we were invited to, for which we did not receive a key bundle before accepting the invite.
* @param inviterId - The user who invited us to the room and is expected to send the room key bundle. * @param inviterId - The user who invited us to the room and is expected to send the room key bundle.
*/ */
markRoomAsPendingKeyBundle(roomId: string, inviterId: string): void; markRoomAsPendingKeyBundle(roomId: string, inviterId: string): Promise<void>;
} }
/** The methods which crypto implementations should expose to the Sync api /** The methods which crypto implementations should expose to the Sync api
+36 -19
View File
@@ -188,7 +188,9 @@ export interface CryptoApi {
/** /**
* Check if the given user has published cross-signing keys. * Check if the given user has published cross-signing keys.
* *
* - If the user is tracked, a `/keys/query` request is made to update locally the cross signing keys. * - If the user is this user, a `/keys/query` request is made to update locally the cross signing keys.
* - If the user is tracked, any current `/keys/query` requests are awaited (with a timeout) and then
* the locally cached information is used.
* - If the user is not tracked locally and downloadUncached is set to true, * - If the user is not tracked locally and downloadUncached is set to true,
* a `/keys/query` request is made to the server to retrieve the cross signing keys. * a `/keys/query` request is made to the server to retrieve the cross signing keys.
* - Otherwise, return false * - Otherwise, return false
@@ -205,7 +207,10 @@ export interface CryptoApi {
* Get the device information for the given list of users. * Get the device information for the given list of users.
* *
* For any users whose device lists are cached (due to sharing an encrypted room with the user), the * For any users whose device lists are cached (due to sharing an encrypted room with the user), the
* cached device data is returned. * cached device data is returned, unless it is stale.
*
* If there are users with stale cached entries, wait (with some timeout) for any in-progress
* `/keys/query` request to complete.
* *
* If there are uncached users, and the `downloadUncached` parameter is set to `true`, * If there are uncached users, and the `downloadUncached` parameter is set to `true`,
* a `/keys/query` request is made to the server to retrieve these devices. * a `/keys/query` request is made to the server to retrieve these devices.
@@ -626,7 +631,6 @@ export interface CryptoApi {
* * Disables 4S, deleting the info for the default key, the default key pointer itself and any * * Disables 4S, deleting the info for the default key, the default key pointer itself and any
* known 4S data (cross-signing keys and the megolm key backup key). * known 4S data (cross-signing keys and the megolm key backup key).
* * Deletes any dehydrated devices. * * Deletes any dehydrated devices.
* * Sets the "m.org.matrix.custom.backup_disabled" account data flag to indicate that the user has disabled backups.
*/ */
disableKeyStorage(): Promise<void>; disableKeyStorage(): Promise<void>;
@@ -720,20 +724,6 @@ export interface CryptoApi {
* @param secrets - The secrets bundle received from the other device * @param secrets - The secrets bundle received from the other device
*/ */
importSecretsBundle?(secrets: Awaited<ReturnType<SecretsBundle["to_json"]>>): Promise<void>; importSecretsBundle?(secrets: Awaited<ReturnType<SecretsBundle["to_json"]>>): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Room key history sharing (MSC4268)
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Share any shareable E2EE history in the given room with the given recipient,
* as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
*
* @experimental
*/
shareRoomHistoryWithUser(roomId: string, userId: string): Promise<void>;
} }
/** A reason code for a failure to decrypt an event. */ /** A reason code for a failure to decrypt an event. */
@@ -808,6 +798,9 @@ export enum DeviceIsolationModeKind {
* *
* Events from all senders are always decrypted (and should be decorated with message shields in case * Events from all senders are always decrypted (and should be decorated with message shields in case
* of authenticity warnings, see {@link EventEncryptionInfo}). * of authenticity warnings, see {@link EventEncryptionInfo}).
*
* `AllDevicesIsolationMode` is used in the legacy, non-'exclude insecure devices' mode in Element Web. It is not
* recommended (see {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}).
*/ */
export class AllDevicesIsolationMode { export class AllDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode; public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
@@ -834,6 +827,9 @@ export class AllDevicesIsolationMode {
* *
* Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption * Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.) * failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
*
* `OnlySignedDevicesIsolationMode` corresponds to the 'Exclude insecure devices' mode in Element Web, which is
* recommended by {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}.
*/ */
export class OnlySignedDevicesIsolationMode { export class OnlySignedDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode; public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
@@ -865,6 +861,25 @@ export interface BootstrapCrossSigningOpts {
* Represents the ways in which we trust a user * Represents the ways in which we trust a user
*/ */
export class UserVerificationStatus { export class UserVerificationStatus {
/**
* Indicates if we have saved a known identity for this user. Typically, this means that we share a
* room with them (or have done in the past).
*
* If this is `false`, then the other flags ({@link isCrossSigningVerified}, {@link wasCrossSigningVerified},
* {@link needsUserApproval}) will also be `false`. This means that we haven't seen this user before.
*
* If this is `true`, then there are further possibilities:
*
* - If {@link isCrossSigningVerified} returns `true`, then we have cryptographically verified the current
* identity of this user: that is the highest form of trust we have.
*
* - If {@link needsUserApproval} is `true`, that means that the user has changed their identity.
*
* - Otherwise, the user is "TOFU trusted": we have a record of their identity, and, typically, will share
* encrypted content with them as long as they retain that identity.
*/
public readonly known: boolean;
/** /**
* Indicates if the identity has changed in a way that needs user approval. * Indicates if the identity has changed in a way that needs user approval.
* *
@@ -880,12 +895,14 @@ export class UserVerificationStatus {
*/ */
public readonly needsUserApproval: boolean; public readonly needsUserApproval: boolean;
/** @internal */
public constructor( public constructor(
private readonly crossSigningVerified: boolean, private readonly crossSigningVerified: boolean,
private readonly crossSigningVerifiedBefore: boolean, private readonly crossSigningVerifiedBefore: boolean,
private readonly tofu: boolean, known: boolean,
needsUserApproval: boolean = false, needsUserApproval: boolean = false,
) { ) {
this.known = known;
this.needsUserApproval = needsUserApproval; this.needsUserApproval = needsUserApproval;
} }
@@ -917,7 +934,7 @@ export class UserVerificationStatus {
* @deprecated No longer supported, with the Rust crypto stack. * @deprecated No longer supported, with the Rust crypto stack.
*/ */
public isTofu(): boolean { public isTofu(): boolean {
return this.tofu; return false;
} }
} }
+1 -1
View File
@@ -806,7 +806,7 @@ export class RoomWidgetClient extends MatrixClient {
// Sliding Sync // Sliding Sync
await this.syncApi!.injectRoomEvents(this.room!, [event]); await this.syncApi!.injectRoomEvents(this.room!, [event]);
} }
logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`); logger.debug(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
} else { } else {
const { event_id: eventId, room_id: roomId } = ev.detail.data; const { event_id: eventId, room_id: roomId } = ev.detail.data;
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`); logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
+1
View File
@@ -82,6 +82,7 @@ export * from "./models/room-summary.ts";
export * from "./models/event-status.ts"; export * from "./models/event-status.ts";
export * from "./models/profile-keys.ts"; export * from "./models/profile-keys.ts";
export * from "./models/related-relations.ts"; export * from "./models/related-relations.ts";
export { type StickyMatrixEvent, RoomStickyEventsEvent } from "./models/room-sticky-events.ts";
export type { RoomSummary } from "./client.ts"; export type { RoomSummary } from "./client.ts";
export * as ContentHelpers from "./content-helpers.ts"; export * as ContentHelpers from "./content-helpers.ts";
export * as SecretStorage from "./secret-storage.ts"; export * as SecretStorage from "./secret-storage.ts";
+2 -1
View File
@@ -863,7 +863,8 @@ function quickFilterNonRelevantContents(content: IContent, logger: Logger): bool
// We have a MSC4143 event membership event with a proper joined content // We have a MSC4143 event membership event with a proper joined content
return true; return true;
} else if (eventKeysCount === 1 && "memberships" in content) { } else if (eventKeysCount === 1 && "memberships" in content) {
logger.warn(`Legacy event found. Those are ignored, they do not contribute to the MatrixRTC session`); // Events used to have this format in the past, but are now deprecated.
// Given that state events ~cannot be deleted, there can be some remaining events in the room, just ignore them.
return false; return false;
} else { } else {
// Invalid or left content // Invalid or left content
+5 -4
View File
@@ -20,7 +20,7 @@ import type { RelationType } from "../../types.ts";
import { type RtcSlotEventContent, type Transport } from "../types.ts"; import { type RtcSlotEventContent, type Transport } from "../types.ts";
import { MatrixRTCMembershipParseError } from "./common.ts"; import { MatrixRTCMembershipParseError } from "./common.ts";
import { sha256 } from "../../digest.ts"; import { sha256 } from "../../digest.ts";
import { encodeUnpaddedBase64Url } from "../../base64.ts"; import { encodeUnpaddedBase64 } from "../../base64.ts";
import { slotIdToDescription } from "../utils.ts"; import { slotIdToDescription } from "../utils.ts";
/** /**
@@ -149,8 +149,9 @@ export const checkRtcMembershipData = (data: IContent, sender: string): data is
}; };
export async function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> { export async function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> {
const hashInput = `${userId}|${deviceId}|${memberId}`; // canonical JSON serialization (Matrix canonical JSON for arrays)
const hashBuffer = await sha256(hashInput); const jsonStr = JSON.stringify([userId, deviceId, memberId]);
const hashedString = encodeUnpaddedBase64Url(hashBuffer); const hashBuffer = await sha256(jsonStr);
const hashedString = encodeUnpaddedBase64(hashBuffer);
return hashedString; return hashedString;
} }
+25 -6
View File
@@ -14,7 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts"; import {
type IdTokenClaims,
Log,
OidcClient,
type SigninRequestCreateArgs,
SigninResponse,
SigninState,
WebStorageStateStore,
} from "oidc-client-ts";
import { logger } from "../logger.ts"; import { logger } from "../logger.ts";
import { secureRandomString } from "../randomstring.ts"; import { secureRandomString } from "../randomstring.ts";
@@ -127,6 +135,8 @@ export const generateAuthorizationUrl = async (
* @param urlState - value to append to the opaque state identifier to uniquely identify the callback * @param urlState - value to append to the opaque state identifier to uniquely identify the callback
* @param loginHint - value to send as the `login_hint` to the OP, giving a hint about the login identifier the user might use to log in. * @param loginHint - value to send as the `login_hint` to the OP, giving a hint about the login identifier the user might use to log in.
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}. * See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
* @param responseMode - value to send as the `response_mode` to the OP, selecting how auth is passed back during redirect.
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
* @returns a Promise with the url as a string * @returns a Promise with the url as a string
*/ */
export const generateOidcAuthorizationUrl = async ({ export const generateOidcAuthorizationUrl = async ({
@@ -139,6 +149,7 @@ export const generateOidcAuthorizationUrl = async ({
prompt, prompt,
urlState, urlState,
loginHint, loginHint,
responseMode = "query",
}: { }: {
clientId: string; clientId: string;
metadata: ValidatedAuthMetadata; metadata: ValidatedAuthMetadata;
@@ -149,6 +160,7 @@ export const generateOidcAuthorizationUrl = async ({
prompt?: string; prompt?: string;
urlState?: string; urlState?: string;
loginHint?: string; loginHint?: string;
responseMode?: SigninRequestCreateArgs["response_mode"];
}): Promise<string> => { }): Promise<string> => {
const scope = generateScope(); const scope = generateScope();
const oidcClient = new OidcClient({ const oidcClient = new OidcClient({
@@ -156,7 +168,7 @@ export const generateOidcAuthorizationUrl = async ({
client_id: clientId, client_id: clientId,
redirect_uri: redirectUri, redirect_uri: redirectUri,
authority: metadata.issuer, authority: metadata.issuer,
response_mode: "query", response_mode: responseMode,
response_type: "code", response_type: "code",
scope, scope,
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }), stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
@@ -200,7 +212,8 @@ const normalizeBearerTokenResponseTokenType = (response: SigninResponse): Bearer
* request to the Token Endpoint, to obtain the access token, refresh token, etc. * request to the Token Endpoint, to obtain the access token, refresh token, etc.
* *
* @param code - authorization code as returned by OP during authorization * @param code - authorization code as returned by OP during authorization
* @param storedAuthorizationParams - stored params from start of oidc login flow * @param state - authorization state param as returned by OP during authorization
* @param responseMode - the response mode used for authentication
* @returns valid bearer token response * @returns valid bearer token response
* @throws An `Error` with `message` set to an entry in {@link OidcError}, * @throws An `Error` with `message` set to an entry in {@link OidcError},
* when the request fails, or the returned token response is invalid. * when the request fails, or the returned token response is invalid.
@@ -208,6 +221,7 @@ const normalizeBearerTokenResponseTokenType = (response: SigninResponse): Bearer
export const completeAuthorizationCodeGrant = async ( export const completeAuthorizationCodeGrant = async (
code: string, code: string,
state: string, state: string,
responseMode: SigninRequestCreateArgs["response_mode"] = "query",
): Promise<{ ): Promise<{
oidcClientSettings: { clientId: string; issuer: string }; oidcClientSettings: { clientId: string; issuer: string };
tokenResponse: BearerTokenResponse; tokenResponse: BearerTokenResponse;
@@ -221,13 +235,18 @@ export const completeAuthorizationCodeGrant = async (
* so that oidc-client can parse it * so that oidc-client can parse it
*/ */
const reconstructedUrl = new URL(window.location.origin); const reconstructedUrl = new URL(window.location.origin);
reconstructedUrl.searchParams.append("code", code);
reconstructedUrl.searchParams.append("state", state); const params = new URLSearchParams({ code, state });
if (responseMode === "query") {
reconstructedUrl.search = params.toString();
} else {
reconstructedUrl.hash = `#${params.toString()}`;
}
// set oidc-client to use our logger // set oidc-client to use our logger
Log.setLogger(logger); Log.setLogger(logger);
try { try {
const response = new SigninResponse(reconstructedUrl.searchParams); const response = new SigninResponse(params);
const stateStore = new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }); const stateStore = new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage });
+1 -1
View File
@@ -62,6 +62,6 @@ export const validateAuthMetadataAndKeys = async (authMetadata: unknown): Promis
return { return {
...validatedIssuerConfig, ...validatedIssuerConfig,
signingKeys: await metadataService.getSigningKeys(), signingKeys: validatedIssuerConfig.jwks_uri ? await metadataService.getSigningKeys() : null,
}; };
}; };
+6 -6
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm"; import { QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
import { import {
ClientRendezvousFailureReason, ClientRendezvousFailureReason,
@@ -108,7 +108,7 @@ interface SecretsPayload extends MSC4108Payload, Awaited<ReturnType<NonNullable<
* @experimental Note that this is UNSTABLE and may have breaking changes without notice. * @experimental Note that this is UNSTABLE and may have breaking changes without notice.
*/ */
export class MSC4108SignInWithQR { export class MSC4108SignInWithQR {
private readonly ourIntent: QrCodeMode; private readonly ourIntent: QrCodeIntent;
private _code?: Uint8Array; private _code?: Uint8Array;
private expectingNewDeviceId?: string; private expectingNewDeviceId?: string;
@@ -131,7 +131,7 @@ export class MSC4108SignInWithQR {
private readonly client?: MatrixClient, private readonly client?: MatrixClient,
public onFailure?: RendezvousFailureListener, public onFailure?: RendezvousFailureListener,
) { ) {
this.ourIntent = client ? QrCodeMode.Reciprocate : QrCodeMode.Login; this.ourIntent = client ? QrCodeIntent.Reciprocate : QrCodeIntent.Login;
} }
/** /**
@@ -149,9 +149,9 @@ export class MSC4108SignInWithQR {
return; return;
} }
if (this.ourIntent === QrCodeMode.Reciprocate && this.client) { if (this.ourIntent === QrCodeIntent.Reciprocate && this.client) {
this._code = await this.channel.generateCode(this.ourIntent, this.client.getDomain()!); this._code = await this.channel.generateCode(this.ourIntent, this.client.getDomain()!);
} else if (this.ourIntent === QrCodeMode.Login) { } else if (this.ourIntent === QrCodeIntent.Login) {
this._code = await this.channel.generateCode(this.ourIntent); this._code = await this.channel.generateCode(this.ourIntent);
} }
} }
@@ -160,7 +160,7 @@ export class MSC4108SignInWithQR {
* Returns true if the device is the already logged in device reciprocating a new login on the other side of the channel. * Returns true if the device is the already logged in device reciprocating a new login on the other side of the channel.
*/ */
public get isExistingDevice(): boolean { public get isExistingDevice(): boolean {
return this.ourIntent === QrCodeMode.Reciprocate; return this.ourIntent === QrCodeIntent.Reciprocate;
} }
/** /**
@@ -19,7 +19,7 @@ import {
Ecies, Ecies,
type EstablishedEcies, type EstablishedEcies,
QrCodeData, QrCodeData,
QrCodeMode, QrCodeIntent,
} from "@matrix-org/matrix-sdk-crypto-wasm"; } from "@matrix-org/matrix-sdk-crypto-wasm";
import { import {
@@ -56,9 +56,9 @@ export class MSC4108SecureChannel {
* @param mode the mode to generate the QR code in, either `Login` or `Reciprocate`. * @param mode the mode to generate the QR code in, either `Login` or `Reciprocate`.
* @param serverName the name of the homeserver to connect to, as defined by server discovery in the spec, required for `Reciprocate` mode. * @param serverName the name of the homeserver to connect to, as defined by server discovery in the spec, required for `Reciprocate` mode.
*/ */
public async generateCode(mode: QrCodeMode.Login): Promise<Uint8Array>; public async generateCode(mode: QrCodeIntent.Login): Promise<Uint8Array>;
public async generateCode(mode: QrCodeMode.Reciprocate, serverName: string): Promise<Uint8Array>; public async generateCode(mode: QrCodeIntent.Reciprocate, serverName: string): Promise<Uint8Array>;
public async generateCode(mode: QrCodeMode, serverName?: string): Promise<Uint8Array> { public async generateCode(mode: QrCodeIntent, serverName?: string): Promise<Uint8Array> {
const { url } = this.rendezvousSession; const { url } = this.rendezvousSession;
if (!url) { if (!url) {
@@ -68,7 +68,7 @@ export class MSC4108SecureChannel {
return new QrCodeData( return new QrCodeData(
this.secureChannel.public_key(), this.secureChannel.public_key(),
url, url,
mode === QrCodeMode.Reciprocate ? serverName : undefined, mode === QrCodeIntent.Reciprocate ? serverName : undefined,
).toBytes(); ).toBytes();
} }
+31 -10
View File
@@ -161,7 +161,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
/** /**
* Handles a backup secret received event and store it if it matches the current backup version. * Handles a backup secret received event and store it if it matches the current backup version.
* *
* @param secret - The secret as received from a `m.secret.send` event for secret `m.megolm_backup.v1`. * @param secret - The secret as received from a `m.secret.send` or `io.element.msc4385.secret.push` event for secret `m.megolm_backup.v1`.
* @returns true if the secret is valid and has been stored, false otherwise. * @returns true if the secret is valid and has been stored, false otherwise.
*/ */
public async handleBackupSecretReceived(secret: string): Promise<boolean> { public async handleBackupSecretReceived(secret: string): Promise<boolean> {
@@ -180,28 +180,44 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
// There is no server-side key backup. // There is no server-side key backup.
// This decryption key is useless to us. // This decryption key is useless to us.
this.logger.warn( this.logger.warn(
"handleBackupSecretReceived: Received a backup decryption key, but there is no trusted server-side key backup", "handleBackupSecretReceived: Received a backup decryption key, but there is no server-side key backup",
); );
return false; return false;
} }
let backupDecryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
try {
backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(secret);
} catch (e) {
this.logger.warn("handleBackupSecretReceived: Invalid backup decryption key", e);
return false;
}
try { try {
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(secret);
const privateKeyMatches = this.backupInfoMatchesBackupDecryptionKey(latestBackupInfo, backupDecryptionKey); const privateKeyMatches = this.backupInfoMatchesBackupDecryptionKey(latestBackupInfo, backupDecryptionKey);
if (!privateKeyMatches) { if (!privateKeyMatches) {
this.logger.warn( this.logger.warn(
`handleBackupSecretReceived: Private decryption key does not match the public key of the current remote backup.`, `handleBackupSecretReceived: Private decryption key does not match the public key of the current server-side backup version (${latestBackupInfo.version})`,
); );
// just ignore the secret // just ignore the secret
return false; return false;
} }
this.logger.info( this.logger.info(
`handleBackupSecretReceived: A valid backup decryption key has been received and stored in cache.`, `handleBackupSecretReceived: Valid decryption key for the current server-side backup version (${latestBackupInfo.version}) received`,
); );
await this.saveBackupDecryptionKey(backupDecryptionKey, latestBackupInfo.version); await this.saveBackupDecryptionKey(backupDecryptionKey, latestBackupInfo.version);
// Check if the backup should be enabled (e.g. if it's properly
// signed), and enable it if it should
if (this.keyBackupCheckInProgress) {
await this.keyBackupCheckInProgress;
}
this.keyBackupCheckInProgress = this.doCheckKeyBackup(latestBackupInfo).finally(() => {
this.keyBackupCheckInProgress = null;
});
await this.keyBackupCheckInProgress;
return true; return true;
} catch (e) { } catch (e) {
this.logger.warn("handleBackupSecretReceived: Invalid backup decryption key", e); this.logger.warn("handleBackupSecretReceived: Unable to validate backup decryption key", e);
} }
return false; return false;
@@ -281,12 +297,17 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null; private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
/** Helper for `checkKeyBackup` */ /** Helper to check the key backup status, and enable/disable it as appropriate
private async doCheckKeyBackup(): Promise<KeyBackupCheck | null> { *
* A KeyBackupInfo can be passed if it was fetched recently, to avoid trying to
* re-fetch it from the server.
*/
private async doCheckKeyBackup(backupInfo?: KeyBackupInfo | null | undefined): Promise<KeyBackupCheck | null> {
this.logger.debug("Checking key backup status..."); this.logger.debug("Checking key backup status...");
let backupInfo: KeyBackupInfo | null | undefined;
try { try {
backupInfo = await this.requestKeyBackupVersion(); if (!backupInfo) {
backupInfo = await this.requestKeyBackupVersion();
}
} catch (e) { } catch (e) {
this.logger.warn("Error checking for active key backup", e); this.logger.warn("Error checking for active key backup", e);
this.serverBackupInfo = undefined; this.serverBackupInfo = undefined;
+17 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { StoreHandle } from "@matrix-org/matrix-sdk-crypto-wasm"; import { StoreHandle } from "@matrix-org/matrix-sdk-crypto-wasm";
import { RustCrypto } from "./rust-crypto.ts"; import { MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE, RustCrypto } from "./rust-crypto.ts";
import { type IHttpOpts, type MatrixHttpApi } from "../http-api/index.ts"; import { type IHttpOpts, type MatrixHttpApi } from "../http-api/index.ts";
import { type ServerSideSecretStorage } from "../secret-storage.ts"; import { type ServerSideSecretStorage } from "../secret-storage.ts";
import { type Logger } from "../logger.ts"; import { type Logger } from "../logger.ts";
@@ -247,5 +247,21 @@ async function initOlmMachine(
} }
} }
// If we have any recently-joined rooms, see if we have a pending key bundle for them.
for (const pendingDetails of await olmMachine.getAllRoomsPendingKeyBundles()) {
const roomId = pendingDetails.roomId.toString();
if (Date.now() - pendingDetails.inviteAcceptedAtMillis <= MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE) {
logger.info(
`Checking for pending key bundle for recently-joined room ${roomId} (joined ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()})`,
);
await rustCrypto.maybeAcceptKeyBundle(roomId, pendingDetails.inviterId.toString());
} else {
logger.info(
`Clearing pending-key-bundle flag for room ${roomId} (too old: joined ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()})`,
);
await olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId));
}
}
return rustCrypto; return rustCrypto;
} }
+133 -53
View File
@@ -98,6 +98,7 @@ import { VerificationMethod } from "../types.ts";
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts"; import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
import { type UIAuthCallback } from "../interactive-auth.ts"; import { type UIAuthCallback } from "../interactive-auth.ts";
import { getHttpUriForMxc } from "../content-repo.ts"; import { getHttpUriForMxc } from "../content-repo.ts";
import { type RoomState } from "../matrix.ts";
const ALL_VERIFICATION_METHODS = [ const ALL_VERIFICATION_METHODS = [
VerificationMethod.Sas, VerificationMethod.Sas,
@@ -111,6 +112,9 @@ interface ISignableObject {
unsigned?: object; unsigned?: object;
} }
/** The maximum time, in milliseconds, since we accepted an invite, that we should accept a key bundle. */
export const MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE = 24 * 60 * 60 * 1000; // 24 hours
/** /**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
* *
@@ -131,9 +135,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/** mapping of roomId → encryptor class */ /** mapping of roomId → encryptor class */
private roomEncryptors: Record<string, RoomEncryptor> = {}; private roomEncryptors: Record<string, RoomEncryptor> = {};
/** mapping of room ID -> inviter ID for rooms pending MSC4268 key bundles */
private readonly roomsPendingKeyBundles: Map<string, string> = new Map();
private eventDecryptor: EventDecryptor; private eventDecryptor: EventDecryptor;
private keyClaimManager: KeyClaimManager; private keyClaimManager: KeyClaimManager;
private outgoingRequestProcessor: OutgoingRequestProcessor; private outgoingRequestProcessor: OutgoingRequestProcessor;
@@ -370,10 +371,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/* allowRedirects */ true, /* allowRedirects */ true,
/* useAuthentication */ true, /* useAuthentication */ true,
); );
let encryptedBundle: Blob; let encryptedBundle: Uint8Array;
try { try {
const bundleUrl = new URL(url); const bundleUrl = new URL(url);
encryptedBundle = await this.http.authedRequest<Blob>( const encryptedBundleBlob = await this.http.authedRequest<Blob>(
Method.Get, Method.Get,
bundleUrl.pathname + bundleUrl.search, bundleUrl.pathname + bundleUrl.search,
{}, {},
@@ -383,17 +384,24 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
prefix: "", prefix: "",
}, },
); );
logger.info(`Received blob of length ${encryptedBundleBlob.size}`);
encryptedBundle = new Uint8Array(await encryptedBundleBlob.arrayBuffer());
} catch (err) { } catch (err) {
logger.warn(`Error downloading encrypted bundle from ${url}:`, err); logger.warn(`Error downloading encrypted bundle from ${url}:`, err);
throw err; throw err;
} }
logger.info(`Received blob of length ${encryptedBundle.size}`);
try { try {
await this.olmMachine.receiveRoomKeyBundle(bundleData, new Uint8Array(await encryptedBundle.arrayBuffer())); await this.olmMachine.receiveRoomKeyBundle(bundleData, encryptedBundle);
} catch (err) { } catch (err) {
logger.warn(`Error receiving encrypted bundle:`, err); logger.warn(`Error receiving encrypted bundle:`, err);
throw err; throw err;
} finally {
// Even if we were unable to import the bundle, we still clear the flag that indicates that we
// are waiting for the bundle to be received. The only reason this can happen is that the bundle was
// malformed somehow, so we don't want to keep retrying it.
await this.olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId));
} }
return true; return true;
@@ -402,8 +410,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/** /**
* Implementation of {@link CryptoBackend.markRoomAsPendingKeyBundle}. * Implementation of {@link CryptoBackend.markRoomAsPendingKeyBundle}.
*/ */
public markRoomAsPendingKeyBundle(roomId: string, inviter: string): void { public async markRoomAsPendingKeyBundle(roomId: string, inviter: string): Promise<void> {
this.roomsPendingKeyBundles.set(roomId, inviter); await this.olmMachine.storeRoomPendingKeyBundle(
new RustSdkCryptoJs.RoomId(roomId),
new RustSdkCryptoJs.UserId(inviter),
);
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -725,7 +736,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
? userIdentity.identityNeedsUserApproval() ? userIdentity.identityNeedsUserApproval()
: false; : false;
userIdentity.free(); userIdentity.free();
return new UserVerificationStatus(verified, wasVerified, false, needsUserApproval); return new UserVerificationStatus(verified, wasVerified, true, needsUserApproval);
} }
/** /**
@@ -781,9 +792,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#getCrossSigningKeyId} * Implementation of {@link CryptoApi#getCrossSigningKeyId}
*/ */
public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> { public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> {
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity( const userIdentity = await this.getOwnIdentity();
new RustSdkCryptoJs.UserId(this.userId),
);
if (!userIdentity) { if (!userIdentity) {
// The public keys are not available on this device // The public keys are not available on this device
return null; return null;
@@ -1012,9 +1021,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#getCrossSigningStatus} * Implementation of {@link CryptoApi#getCrossSigningStatus}
*/ */
public async getCrossSigningStatus(): Promise<CrossSigningStatus> { public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | null = await this.getOlmMachineOrThrow().getIdentity( const userIdentity = await this.getOwnIdentity();
new RustSdkCryptoJs.UserId(this.userId),
);
const publicKeysOnDevice = const publicKeysOnDevice =
Boolean(userIdentity?.masterKey) && Boolean(userIdentity?.masterKey) &&
@@ -1128,9 +1135,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#requestVerificationDM} * Implementation of {@link CryptoApi#requestVerificationDM}
*/ */
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> { public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | undefined = await this.olmMachine.getIdentity( const userIdentity = (await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(userId))) as
new RustSdkCryptoJs.UserId(userId), | RustSdkCryptoJs.OtherUserIdentity
); | undefined;
if (!userIdentity) throw new Error(`unknown userId ${userId}`); if (!userIdentity) throw new Error(`unknown userId ${userId}`);
@@ -1214,9 +1221,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* @returns a VerificationRequest when the request has been sent to the other party. * @returns a VerificationRequest when the request has been sent to the other party.
*/ */
public async requestOwnUserVerification(): Promise<VerificationRequest> { public async requestOwnUserVerification(): Promise<VerificationRequest> {
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity( const userIdentity = await this.getOwnIdentity();
new RustSdkCryptoJs.UserId(this.userId),
);
if (userIdentity === undefined) { if (userIdentity === undefined) {
throw new Error("cannot request verification for this device when there is no existing cross-signing key"); throw new Error("cannot request verification for this device when there is no existing cross-signing key");
} }
@@ -1371,6 +1376,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
public async resetKeyBackup(): Promise<void> { public async resetKeyBackup(): Promise<void> {
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o)); const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
await this.pushSecretToVerifiedDevices("m.megolm_backup.v1");
// we want to store the private key in 4S // we want to store the private key in 4S
// need to check if 4S is set up? // need to check if 4S is set up?
if (await this.secretStorageHasAESKey()) { if (await this.secretStorageHasAESKey()) {
@@ -1726,34 +1733,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
}, },
}); });
// If we have received a room key bundle message, and have previously marked the room // If we have received a room key bundle message, and have recently joined the room in question,
// IDs it references as pending key bundles, tell the Rust SDK to try and accept it, // tell the Rust SDK to try and accept the key bundle.
// just in case it was received after invite.
// //
// We don't actually need to validate the contents of the bundle message, or do // We don't actually need to validate the contents of the bundle message, or do
// anything with its contents at all. We simply want to inform the Rust SDK we have // anything with its contents at all. We simply want to inform the Rust SDK we have
// received a new room key bundle that we might be able to download. // received a new room key bundle that we might be able to download.
if ( if (isRoomKeyBundleMessage(parsedMessage)) {
isRoomKeyBundleMessage(parsedMessage) && const roomId = parsedMessage.content.room_id;
this.roomsPendingKeyBundles.has(parsedMessage.content.room_id) const pendingDetails = await this.olmMachine.getPendingKeyBundleDetailsForRoom(
) { new RustSdkCryptoJs.RoomId(roomId),
// No `await`-ing here, as this is called from inside the `/sync` loop.
this.maybeAcceptKeyBundle(
parsedMessage.content.room_id,
this.roomsPendingKeyBundles.get(parsedMessage.content.room_id)!,
).then(
(success) => {
if (success) {
this.roomsPendingKeyBundles.delete(parsedMessage.content.room_id);
}
},
(err) => {
this.logger.error(
`Error attempting to download key bundle for room ${parsedMessage.content.room_id}`,
);
this.logger.error(err);
},
); );
// Only accept the key bundle if we joined the room less than 24 hours ago.
if (!pendingDetails) {
this.logger.debug(
`Not yet accepting key bundle for room where we are not awaiting a bundle: ${roomId}`,
);
} else if (
Date.now() - pendingDetails.inviteAcceptedAtMillis >
MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE
) {
this.logger.info(
`Ignoring key bundle for room we joined too long ago: ${roomId}, joining time: ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()}`,
);
} else {
this.logger.info(`Considering key bundle for recently-joined room ${roomId}`);
// Don't block for the import to happen, here, as this is called from inside the `/sync` loop.
this.maybeAcceptKeyBundle(roomId, pendingDetails.inviterId.toString()).catch((err) => {
this.logger.error(`Error attempting to download key bundle for room ${roomId}`);
this.logger.error(err);
});
}
} }
break; break;
@@ -1926,7 +1936,21 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* @param oldMembership - The previous membership state. Null if it's a new member. * @param oldMembership - The previous membership state. Null if it's a new member.
*/ */
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void { public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
const enc = this.roomEncryptors[event.getRoomId()!]; const roomId = event.getRoomId()!;
// If it's our own membership, and we are no longer joined, clear any indication that we are waiting for a key
// bundle.
if (
oldMembership === KnownMembership.Join &&
member.membership !== KnownMembership.Join &&
member.userId === this.olmMachine.userId.toString()
) {
this.olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId)).catch((e) => {
this.logger.error(`Error clearing room pending key bundle indicator for ${roomId}: ${e}`);
});
}
const enc = this.roomEncryptors[roomId];
if (!enc) { if (!enc) {
// not encrypting in this room // not encrypting in this room
return; return;
@@ -1934,6 +1958,44 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
enc.onRoomMembership(member); enc.onRoomMembership(member);
} }
/**
* Previously, it was sufficient to check if we need to rotate the room key
* prior to sending a message. However, the history sharing feature
* (MSC4268) breaks this logic:
*
* 1. Alice sends a message M1 in room X;
* 2. Bob invites Charlie, who joins and immediately leaves the room;
* 3. Alice sends another message M2 in room X.
*
* Under the old logic, Alice would not rotate her key after Charlie
* leaves, resulting in M2 being encrypted with the same session as M1.
* This would allow Charlie to decrypt M2 if he ever gains access to
* the event.
*
* To counter this, we proactively discard any active outgoing Megolm
* session when we see an event indicating the user left.
*
* Note that we have to do this in `onRoomStateEvent` rather than
* `onRoomMembership`, because `onRoomMembership` is only called when we see
* a *change* in membership. In the case of a gappy sync, we might miss
* Charlie's invite and join, and only see the final `leave` event (so his
* membership goes from `leave` to `leave`).
*/
public onRoomStateEvent(event: MatrixEvent, _state: RoomState, _prevEvent: MatrixEvent | null): void {
if (event.getType() != EventType.RoomMember) {
// Ignore all events that aren't member updates.
return;
}
if (
event.getStateKey()! !== this.olmMachine.userId.toString() &&
event.getContent().membership !== KnownMembership.Join
) {
this.logger.info(`Rotating session for room ${event.getRoomId()} due to member leaving the room`);
this.forceDiscardSession(event.getRoomId()!);
}
}
/** Callback for OlmMachine.registerRoomKeyUpdatedCallback /** Callback for OlmMachine.registerRoomKeyUpdatedCallback
* *
* Called by the rust-sdk whenever there is an update to (megolm) room keys. We * Called by the rust-sdk whenever there is an update to (megolm) room keys. We
@@ -2044,9 +2106,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/** /**
* Handles secret received from the rust secret inbox. * Handles secret received from the rust secret inbox.
* *
* The gossipped secrets are received using the `m.secret.send` event type * The gossipped secrets are received using the `m.secret.send` or
* and are guaranteed to have been received over a 1-to-1 Olm * `io.element.msc4385.secret.push` event types and are guaranteed to have
* Session from a verified device. * been received over a 1-to-1 Olm Session from a verified device.
* *
* The only secret currently handled in this way is `m.megolm_backup.v1`. * The only secret currently handled in this way is `m.megolm_backup.v1`.
* *
@@ -2189,7 +2251,22 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Used during migration from legacy js-crypto to update local trust if needed. * Used during migration from legacy js-crypto to update local trust if needed.
*/ */
public async getOwnIdentity(): Promise<RustSdkCryptoJs.OwnUserIdentity | undefined> { public async getOwnIdentity(): Promise<RustSdkCryptoJs.OwnUserIdentity | undefined> {
return await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId)); const identity = (await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(this.userId))) as
| RustSdkCryptoJs.OwnUserIdentity
| undefined;
return identity;
}
/**
* Push a secret to all of the current user's verified devices.
*/
public async pushSecretToVerifiedDevices(name: string): Promise<void> {
const logger = new LogSpan(this.logger, "pushSecretToVerifiedDevices");
await this.keyClaimManager.ensureSessionsForUsers(logger, [new RustSdkCryptoJs.UserId(this.userId)]);
await this.olmMachine.pushSecretToVerifiedDevices(name);
this.outgoingRequestsManager.doProcessOutgoingRequests().catch((e) => {
logger.warn("pushSecretToVerifiedDevices: Error processing outgoing requests", e);
});
} }
} }
@@ -2519,7 +2596,7 @@ function rustEncryptionInfoToJsEncryptionInfo(
} }
interface RoomKeyBundleMessage { interface RoomKeyBundleMessage {
type: "io.element.msc4268.room_key_bundle"; type: "m.room_key_bundle" | "io.element.msc4268.room_key_bundle";
content: { content: {
room_id: string; room_id: string;
}; };
@@ -2529,13 +2606,16 @@ interface RoomKeyBundleMessage {
* Determines if the given payload is a RoomKeyBundleMessage. * Determines if the given payload is a RoomKeyBundleMessage.
* *
* A RoomKeyBundleMessage is identified by having a specific message type * A RoomKeyBundleMessage is identified by having a specific message type
* ("io.element.msc4268.room_key_bundle") and a valid room_id in its content. * ("m.room_key_bundle") and a valid room_id in its content.
* *
* @param message - The received to-device message to check. * @param message - The received to-device message to check.
* @returns True if the payload matches the RoomKeyBundleMessage structure, false otherwise. * @returns True if the payload matches the RoomKeyBundleMessage structure, false otherwise.
*/ */
function isRoomKeyBundleMessage(message: IToDeviceEvent): message is IToDeviceEvent & RoomKeyBundleMessage { function isRoomKeyBundleMessage(message: IToDeviceEvent): message is IToDeviceEvent & RoomKeyBundleMessage {
return message.type === "io.element.msc4268.room_key_bundle" && typeof message.content.room_id === "string"; return (
(message.type === "io.element.msc4268.room_key_bundle" || message.type === "m.room_key_bundle") &&
typeof message.content.room_id === "string"
);
} }
type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent]; type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
+2 -1
View File
@@ -61,6 +61,7 @@ const VERSION = DB_MIGRATIONS.length;
* Return the data you want to keep. * Return the data you want to keep.
* @returns Promise which resolves to an array of whatever you returned from * @returns Promise which resolves to an array of whatever you returned from
* resultMapper. * resultMapper.
* @throws If there was an error completing the query.
*/ */
function selectQuery<T>( function selectQuery<T>(
store: IDBObjectStore, store: IDBObjectStore,
@@ -71,7 +72,7 @@ function selectQuery<T>(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const results: T[] = []; const results: T[] = [];
query.onerror = (): void => { query.onerror = (): void => {
reject(new Error("Query failed: " + query.error?.name)); reject(new Error(`selectQuery failed for ${store.name}`, { cause: query.error }));
}; };
// collect results // collect results
query.onsuccess = (): void => { query.onsuccess = (): void => {
+1 -2
View File
@@ -21,7 +21,6 @@ limitations under the License.
* This is an internal module. See {@link createNewMatrixCall} for the public API. * This is an internal module. See {@link createNewMatrixCall} for the public API.
*/ */
import { v4 as uuidv4 } from "uuid";
import { parse as parseSdp, write as writeSdp } from "sdp-transform"; import { parse as parseSdp, write as writeSdp } from "sdp-transform";
import { logger } from "../logger.ts"; import { logger } from "../logger.ts";
@@ -2490,7 +2489,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
sender_session_id: this.client.getSessionId(), sender_session_id: this.client.getSessionId(),
dest_session_id: this.opponentSessionId, dest_session_id: this.opponentSessionId,
seq: toDeviceSeq, seq: toDeviceSeq,
[ToDeviceMessageId]: uuidv4(), [ToDeviceMessageId]: globalThis.crypto.randomUUID(),
}; };
this.emit( this.emit(