Compare commits

...

144 Commits

Author SHA1 Message Date
RiotRobot 6b0d2a4ad3 v36.2.0-rc.0 2025-02-04 12:29:59 +00:00
Michael Telatynski 72519a0eb4 Make sonarcloud.yml more reusable (#4681)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-03 14:25:56 +00:00
Will Hunt 624b8a242e Fix topic types (#4678) 2025-02-03 09:03:58 +00:00
Michael Telatynski 5be104a35c Fix types around Terms (#4674) 2025-02-03 08:48:00 +00:00
Will Hunt c93128ed39 Handle empty m.room.topic (#4673)
* Define topic as optional.

* Change isProvided so that types work better.

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

* linting

* Remove usage of optional

* Topic key may only contain legacy key.

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

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

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

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

* feat(dehydrated): fix unneeded call getCachedKey

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

* review: quick fix and doc

* apply changes from review

* apply changes from review

* fix comment

* add some tests and emit an event on rehydration failure

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

* adjust test to cover a few more lines

* fix documentation

* Apply suggestions from code review

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

* fix missing bracket

* add test for getting the dehydration key from SSSS

---------

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

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

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

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

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

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

* docs(crypto api): Review changes

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

check that `importCrossSigningKeys` has actually worked

* Update src/rust-crypto/CrossSigningIdentity.ts

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

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

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

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

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

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

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

* Add missing awaits

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

* review

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

* more details and cleanup

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

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

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

* docstrings for getFocusInUse and getActiveFocus

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

* convert interface to abstract class.

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

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

* move update condition into MembershipManager

* renaming public api

* Update src/matrixrtc/MatrixRTCSession.ts

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

* Update src/matrixrtc/MatrixRTCSession.ts

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

---------

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

* Add MyMembershipManager

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

* Refactor MatrixRTCSession to use myMembershipManager

* fix tests

* review

* get rid of more memberhsip manager usage in tests

* review - fix tests using private membershipManager props

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

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

* Iterate

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

* Simplify change

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

* Add test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-10 10:16:45 +00:00
renovate[bot] 5b85ae491e Update typescript-eslint monorepo to v8.19.0 (#4607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 14:02:30 +00:00
renovate[bot] 2024b070b0 Update typedoc (#4604)
* Update typedoc

* Make typedoc happier

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-01-07 13:39:07 +00:00
renovate[bot] eef964f07d Update dependency @stylistic/eslint-plugin to v2.12.1 (#4606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 13:16:07 +00:00
renovate[bot] 1e9c119159 Update dependency @types/node to v18.19.69 (#4603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 12:55:34 +00:00
renovate[bot] fd894309f2 Update guibranco/github-status-action-v2 digest to 56cd38c (#4602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 12:54:50 +00:00
renovate[bot] 75486b72a6 Update all non-major dependencies (#4605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 12:53:53 +00:00
RiotRobot 38816589f5 v36.0.0-rc.0 2025-01-07 12:43:09 +00:00
Timo 6f743bfa1f MatrixRTC: Implement expiry logic for CallMembership and additional test coverage (#4587)
* remove all legacy call related code and adjust tests.
We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests.

* dont adjust tests but remove legacy tests

* Remove deprecated CallMembership.getLocalExpiry()

* Remove references to legacy in test case names

* Clean up SessionMembershipData tsdoc

* Remove CallMembership.expires

* Use correct expire duration.

* make expiration methods not return optional values and update docstring

* add docs to `SessionMembershipData`

* Add new tests for session type member events that before only existed for legacy member events.

This reverts commit 795a3cffb61d672941c49e8139eb1d7b15c87d73.

* remove code we do not need yet.

* Cleanup

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2025-01-07 10:14:09 +00:00
Timo ffd3c9575e Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership (#4583)
* remove all legacy call related code and adjust tests.
We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests.

* dont adjust tests but remove legacy tests

* Remove deprecated CallMembership.getLocalExpiry()

* Remove references to legacy in test case names

* Clean up SessionMembershipData tsdoc

* Remove CallMembership.expires

* Use correct expire duration.

* make expiration methods not return optional values and update docstring

* add docs to `SessionMembershipData`

* Use `MSC4143` (instaed of `non-legacy`) wording in comment

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

* Incorporate feedback from review

* Fix test name

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2025-01-06 17:23:16 +00:00
David Baker 7678923e04 Don't retry on 4xx responses (#4601)
* Don't retry on 4xx responses

I'm not sure why this was limited to a small set of 4xx responses.
Nominally, no 4xx request should be retried (in fact the comment
below says this, but then the code didn't quite match it).

This was causing key backup requests to be retried even when the
server responded 404 because the backup in question had been deleted,
meaning the client would retry uselessly and it would take longer for
the client to prompt the user for action.

* Exclude 429s
2025-01-06 13:25:29 +00:00
Johannes Marbach 6f7c74f9ea Add syntax & type check for Node.js example on CI (#4410)
* Add syntax & type check for Node.js example on CI

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

* Fix quotes

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
Co-authored-by: Florian Duros <florianduros@element.io>
2025-01-03 18:49:57 +00:00
Michael Telatynski 3fcc56601b Use mapped types for account data content (#4590)
* Use mapped types around account data events

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

* Iterate

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

* Iterate

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

* Iterate

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

* Iterate

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

* Iterate

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

* Harden types for reading account data too

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

* Correct empty object type

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

* Update src/secret-storage.ts

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

* Iterate

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-12-19 22:53:58 +00:00
Andy Balaam bcf3d56bd5 Upgrade matrix-sdk-crypto-wasm to 12.1.0 (#4596)
... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424
2024-12-19 14:33:25 +00:00
RiotRobot 647a2a1a19 Merge branch 'master' into develop 2024-12-18 14:11:02 +00:00
RiotRobot 1bf8533c03 v35.1.0 2024-12-18 14:10:31 +00:00
David Baker fb2f2dd6d4 Merge pull request #4593 from matrix-org/rav/migration-for-previouslyverified
Upgrade matrix-sdk-crypto-wasm to 1.11.0
2024-12-18 14:01:18 +00:00
Richard van der Hoff d33350ff26 Upgrade matrix-sdk-crypto-wasm to 1.11.0
... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424
2024-12-18 13:29:05 +00:00
RiotRobot 349a86c119 Merge branch 'master' into develop 2024-12-17 13:22:36 +00:00
RiotRobot bee65ff13f v35.0.0 2024-12-17 13:22:10 +00:00
Hugh Nimmo-Smith e4182eb752 Update matrix-sdk-crypto-wasm to 12.0.0 (#4589)
It appears to "just work"... but I might be missing something
2024-12-17 11:24:46 +00:00
David Baker 3219aefc92 Avoid key prompts when resetting crypto (#4586)
* Avoid key prompts when resetting crypto

Attempting to get the backup key out of secret storage can cause
the user to be prompted for their key, which is not helpful if this
is being done as part of a reset. This check was redundant anyway
and we can just overwrite the key with the same value.

Also fix docs and remove check for active backup.

* Fix doc
2024-12-17 09:22:31 +00:00
Richard van der Hoff aba4e690af Improve documentation on various secret-storage related methods (#4585)
* Improve documentation on various secret-storage related methods

* fix link

* Apply suggestions from code review
2024-12-16 13:00:18 +00:00
Liam Diprose 693bb22ba1 Handle when aud OIDC claim is an Array (#4584)
* Handle when `aud` OIDC claim is an Array

The `aud` claim of OIDC id_tokens [can be an array](https://github.com/authts/oidc-client-ts/blob/ce6d694639c58e6a1c80904efdac5eda82b82042/src/Claims.ts#L92) but the existing logic
incorrectly assumes `aud` is always a string.

This PR adds the necessary check.

* Clarify `aud` OIDC claim check

* Fix for prettier

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-16 11:38:34 +00:00
renovate[bot] 315e81b7de Update typescript-eslint monorepo to v8.17.0 (#4581)
* Update typescript-eslint monorepo to v8.17.0

* Fix lint errors

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-12 17:45:39 +00:00
David Baker a0502c5ee5 Save the key backup key to 4S during bootstrapCrossSigning (#4542)
* Save the key backup key to secret storage

When setting up secret storage, if we have a key backup key in cache
(like we do for the cross signing secrets).

* Add test

* Get the key directly from the olmMachine

saves converting it needlessly into a buffer to turn it back into
a base64 string

* Overwrite backup keyin storage if different

* Fix test

* Add integ test

* Test failure case for sonar

* Unused import

* Missed return

* Also check active backup version
2024-12-12 15:03:19 +00:00
Timo d1de32ea27 Only re-prepare MatrixrRTC delayed disconnection event on 404 (#4575)
* Set retry counts of event updating to 1000 (from 1)

 With it being set to one the following issue could occur:

```
// If sending state cancels your own delayed state, prepare another delayed state
// TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state
if (this.disconnectDelayId !== undefined) {
    try {
        const knownDisconnectDelayId = this.disconnectDelayId;
        await resendIfRateLimited(
            () =>
                this.client._unstable_updateDelayedEvent(
                    knownDisconnectDelayId,
                    UpdateDelayedEventAction.Restart,
                ),
            1000,
        );
    } catch (e) {
        logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
        this.disconnectDelayId = undefined;
        await prepareDelayedDisconnection();
    }
}
```
This code looks like the `catch(e)` could never be triggered with 429 (rate limit) because they would be caught by `await resendIfRateLimited`. EXCEPT that this is only happening once: `resendIfRateLimited<T>(func: () => Promise<T>, numRetriesAllowed: number = 1)`. So as soon as the server sends two rate limits in a row we get the following:
 - we get into the `catch(e)`  because of the rate limit
 - we forget about `this.disconnectDelayId = undefined`
 - we start a new delayed event `await prepareDelayedDisconnection();`
 - we do not anymore update the old delayed event which is still running!
 - the running delay event will make us disconnect from the call (call member becomes `{}`)
 - we get into our outher error catching mechanism that resends the new state event
 - this cancels the newly created delay leave event (`await prepareDelayedDisconnection();`)
 - and create another delay leave event.
 - but if we are still reate limited (chances are really high due to the reconnect), this loop will REPEAT

* also check for M_NOT_FOUND

* Leave retry at current level

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2024-12-12 14:58:50 +00:00
renovate[bot] 3ae25427a8 Update dependency @types/node to v18.19.67 (#4578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 09:53:08 +00:00
renovate[bot] 8155b0acfc Update mheap/github-action-required-labels digest to 388fd6a (#4577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 22:11:53 +00:00
renovate[bot] 413c156624 Update guibranco/github-status-action-v2 digest to d469d49 (#4576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 18:12:27 +00:00
renovate[bot] e78a3cec9f Update all non-major dependencies (#4579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 16:50:03 +00:00
renovate[bot] 5998de365d Update dependency @babel/cli to v7.26.4 (#4580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 16:31:44 +00:00
RiotRobot 3de2b9bf80 v35.0.0-rc.0 2024-12-10 15:43:43 +00:00
Hubert Chathi ded87290ce Update matrix-sdk-crypto-wasm to 11.0.0 (#4566)
* Update matrix-sdk-crypto-wasm to 11.0.0

* use `backend` variable to test for rust crypto

* apply changes from review
2024-12-09 23:11:02 +00:00
renovate[bot] cf39595bd7 Update dependency typedoc to v0.27.3 (#4573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 19:21:39 +00:00
Stanislav Demydiuk c90ea985c6 Fix age field check in event echo processing (#3635)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-05 16:08:41 +00:00
Florian Duros c54ca29aa8 Rename initCrypto into initLegacyCrypto (#4567) 2024-12-05 11:08:38 +00:00
Michael Telatynski beb3721e7a Avoid use of Buffer as it does not exist in the Web natively (#4569) 2024-12-04 22:32:09 +00:00
Richard van der Hoff 1cad6f4451 Add multiprocess health warnings to initRustCrypto (#4571) 2024-12-04 14:38:48 +00:00
RiotRobot c4ea57d42d Merge branch 'master' into develop 2024-12-03 12:24:07 +00:00
RiotRobot d3f5526ec0 v34.13.0 2024-12-03 12:23:40 +00:00
renovate[bot] b8e332b53d Update dependency typescript to v5.7.2 (#4553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 11:47:14 +00:00
renovate[bot] ab78acc7da Update typedoc (#4568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 11:19:56 +00:00
Michael Telatynski 051f4e2ab9 Fix release-checks to not use reserved name GITHUB_TOKEN
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 10:47:35 +00:00
Michael Telatynski 8863e42e35 More typescript linting (#3310)
* More typescript linting

* Improve types

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

* Discard changes to src/models/MSC3089TreeSpace.ts

* Discard changes to src/realtime-callbacks.ts

* Fix tests

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

* Improve coverage

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 09:56:52 +00:00
Michael Telatynski bc5246970c Extract release sanity checks to reusable workflow (#4546)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 09:14:45 +00:00
Florian Duros edac6a9983 Replace deprecate imports (#4565)
* Replace deprecate imports

* Deprecate `CryptoBackend.getEventEncryptionInfo`

* Add deprecated alternative
2024-11-29 10:04:20 +00:00
Florian Duros 97ef1dc6df Remove deprecated calls of MatrixClient (#4563) 2024-11-29 09:49:52 +00:00
Florian Duros 125e45c24d Deprecate remaining legacy functions and move CryptoEvent.LegacyCryptoStoreMigrationProgress handler (#4560)
* Deprecate legacy functions in `MatrixClient`

* Move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler in rust crypto

* Remove `olmLib` usage in `MatrixClient`
2024-11-29 09:49:32 +00:00
Hugh Nimmo-Smith 3781b6ebfa Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress (#4561) 2024-11-28 12:05:39 +00:00
Michael Telatynski 8fc77c595a Move bump downstreams to js-sdk specific release workflow (#4547)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 14:15:51 +00:00
David Baker 5bcd26e506 Support MSC4222 state_after (#4487)
* WIP support for state_after

* Fix sliding sync sdk / embedded tests

* Allow both state & state_after to be undefined

Since it must have allowed state to be undefined previously: the test
had it as such.

* Fix limited sync handling

* Need to use state_after being undefined

if state can be undefined anyway

* Make sliding sync sdk tests pass

* Remove deprecated interfaces & backwards-compat code

* Remove useless assignment

* Use updates unstable prefix

* Clarify docs

* Remove additional semi-backwards compatible overload

* Update unstable prefixes

* Iterate

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

* Iterate

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

* Fix test

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

* Iterate

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

* Iterate

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

* Add test for MSC4222 behaviour

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

* Improve coverage

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

* Iterate

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

* Fix tests

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

* Iterate

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

* Tidy

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

* Add comments to explain why things work as they are.

* Fix sync accumulator for state_after sync handling

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

* Add tests

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

* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)"

This reverts commit 957329b218.

* Fix Sync Accumulator toJSON putting start timeline state in state_after field

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

* Update tests

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

* Add test case

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Timo <toger5@hotmail.de>
2024-11-27 11:40:41 +00:00
Timo 66f099b2e7 Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" (#4532)
This reverts commit 957329b218.
2024-11-27 10:49:29 +00:00
renovate[bot] b1445d7457 Update all non-major dependencies (#4558)
* Update all non-major dependencies

* Prettier

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 09:22:49 +00:00
Michael Telatynski b3794760c6 Merge remote-tracking branch 'origin/develop' into develop 2024-11-27 09:10:52 +00:00
Michael Telatynski fbdcc597b6 Fix workflow permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 07:44:35 +00:00
renovate[bot] 8ac32b7894 Update dependency typedoc-plugin-coverage to v3.4.0 (#4559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:51:22 +00:00
renovate[bot] b27c6de78d Update guibranco/github-status-action-v2 digest to 66088c4 (#4548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:23:16 +00:00
renovate[bot] c8e0987774 Update dependency typedoc-plugin-missing-exports to v3.0.2 (#4550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:41:22 +00:00
renovate[bot] 849fcd3341 Update dependency @types/node to v18.19.65 (#4549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:33:27 +00:00
renovate[bot] 8df8266e4a Update dependency @stylistic/eslint-plugin to v2.11.0 (#4552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:16:47 +00:00
renovate[bot] cba4edfd84 Update dependency typedoc-plugin-mdn-links to v4 (#4556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:14:18 +00:00
renovate[bot] 4f50290cd6 Update all non-major dependencies (#4551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 16:35:48 +00:00
renovate[bot] 35e31f02ac Update typescript-eslint monorepo to v8.15.0 (#4554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 16:35:13 +00:00
Hubert Chathi 69647a33b6 Use shield status codes from Rust rather than string matching (#4529) 2024-11-26 15:06:57 +00:00
RiotRobot 13df897896 v34.13.0-rc.0 2024-11-26 13:42:27 +00:00
Michael Telatynski 006929ac0c Fix release workflow permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-26 13:41:14 +00:00
dependabot[bot] 405a6fbb92 Bump smol-toml from 1.3.0 to 1.3.1 (#4544)
Bumps [smol-toml](https://github.com/squirrelchat/smol-toml) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 09:59:55 +00:00
Michael Telatynski d6f3262e12 Prevent releases when release blockers exist (#4543)
* Prevent releases when release blockers exist

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

* Fix

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

* Add permissions

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-26 09:59:41 +00:00
Michael Telatynski 8b32f3eb7f Ensure we disambiguate display names which look like MXIDs (#4540)
* Ensure we disambiguate display names which look like MXIDs

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

* Make tests clearer

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-22 15:52:50 +00:00
Michael Telatynski 1e9934a69d Fix docs-pr-netlify.yaml permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-22 11:35:04 +00:00
Michael Telatynski a6d342d3a4 Switch away from deprecated github-status-action (#4539)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 17:04:22 +00:00
Michael Telatynski 4b9a1bd53f Fix coverage check
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 16:05:38 +00:00
Michael Telatynski 26248f85d5 Additional check to ensure artifacts are downloaded
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 16:00:30 +00:00
Michael Telatynski ae2ae483db We have to explicitly specify github-token even though it is the default for this to work in theory
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 15:59:17 +00:00
Michael Telatynski c87692d0aa Tighten GITHUB_TOKEN permissions (#4538)
* Tighten GITHUB_TOKEN permissions

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

* Tighten GITHUB_TOKEN permissions

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

* Iterate

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

* Fix permission

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 14:01:45 +00:00
Florian Duros 2dd4334e20 Fix RustBackupManager remaining values after current backup removal. #4534 (#4537) 2024-11-21 13:21:37 +00:00
dependabot[bot] 2210255d6f Bump cross-spawn from 7.0.3 to 7.0.6 (#4536)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 13:28:48 +00:00
RiotRobot 544ac86d20 Merge branch 'master' into develop 2024-11-19 14:18:23 +00:00
Florian Duros 781c3b05e5 Add CryptoApi.getBackupInfo (#4512)
* Add `CryptoApi.getBackupInfo`

* improve doc
2024-11-18 13:31:35 +00:00
Timo 325dace437 Fix local echo in embedded mode (#4498)
* fix local echo

* dont use custome event emitter anymore

* move logic into updateTxId

* temp testing

* use generic eventEmtitter names

* add tests

---------

Co-authored-by: Robin <robin@robin.town>
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2024-11-14 13:21:20 +00:00
Michael Telatynski 5c894b34b3 Add downstream tsc for element-web (#4508)
* Add downstream tsc for element-web

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

* Update static_analysis.yml

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-13 14:16:37 +00:00
Florian Duros f5f4091a00 Deprecate MatrixClient.isEventSenderVerified (#4527) 2024-11-13 11:10:38 +00:00
Florian Duros 52bdb57a47 Use crypto api import for CryptoCallbacks instead of legacy crypto (#4526) 2024-11-13 10:47:02 +00:00
Richard van der Hoff 3c23eb69b5 Remove reference to libolm from readme (#4525)
* Remove reference to libolm from readme

* README: fix link to node example
2024-11-13 10:24:49 +00:00
Florian Duros c93b7ce188 Add restoreKeybackup to CryptoApi. (#4476)
* First draft of moving out restoreKeyBackup out of MatrixClient

* Deprecate `restoreKeyBackup*` in `MatrixClient`

* Move types

* Handle only the room keys response

* Renaming and refactor `keysCountInBatch` & `getTotalKeyCount`

* Fix `importRoomKeysAsJson` tsdoc

* Fix typo

* Move `backupDecryptor.free()``

* Comment and simplify a bit `handleDecryptionOfAFullBackup`

* Fix decryption crash by moving`backupDecryptor.free`

* Use new api in `megolm-backup.spec.ts`

* Add tests to get recovery key from secret storage

* Add doc to `KeyBackupRestoreOpts` & `KeyBackupRestoreResult`

* Add doc to `restoreKeyBackupWithKey`

* Add doc to `backup.ts`

* Apply comment suggestions

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

* - Decryption key is recovered from the cache in `RustCrypto.restoreKeyBackup`
- Add `CryptoApi.getSecretStorageBackupPrivateKey` to get the decryption key from the secret storage.

* Add `CryptoApi.restoreKeyBackup` to `ImportRoomKeyProgressData` doc.

* Add deprecated symbol to all the `restoreKeyBackup*` overrides.

* Update tests

* Move `RustBackupManager.getTotalKeyCount` to `backup#calculateKeyCountInKeyBackup`

* Fix `RustBackupManager.restoreKeyBackup` tsdoc

* Move `backupDecryptor.free` in rust crypto.

* Move `handleDecryptionOfAFullBackup` in `importKeyBackup`

* Rename `calculateKeyCountInKeyBackup` to `countKeystInBackup`

* Fix `passphrase` typo

* Rename `backupInfoVersion` to `backupVersion`

* Complete restoreKeyBackup* methods documentation

* Add `loadSessionBackupPrivateKeyFromSecretStorage`

* Remove useless intermediary result variable.

* Check that decryption key matchs key backup info in `loadSessionBackupPrivateKeyFromSecretStorage`

* Get backup info from a specific version

* Fix typo in `countKeysInBackup`

* Improve documentation and naming

* Use `RustSdkCryptoJs.BackupDecryptionKey` as `decryptionKeyMatchesKeyBackupInfo` parameter.

* Call directly `olmMachine.getBackupKeys` in `restoreKeyBackup`

* Last review changes

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-11-13 09:17:32 +00:00
Florian Duros 705b6336cf Update e2e doc in README.md (#4503)
* Update e2e doc in `README.md`

* Update `ICreateClientOpts.cryptoStore` doc

* Apply first batch of suggestion

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

* Fix `cryptoStore` tsdoc in `client.ts`

* Changes in Initialization chapter.

* Add doc about deprecated methods in `MatrixClient`.

* Update SecretStorage doc

* Fis typos

* Improve e2e docs

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-11-12 16:51:01 +00:00
renovate[bot] 7c41e3fb06 Update all non-major dependencies (#4519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 15:47:11 +00:00
renovate[bot] a314e612fa Update dependency @stylistic/eslint-plugin to v2.10.1 (#4520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 15:07:51 +00:00
renovate[bot] ac6cad2852 Update babel monorepo (#4517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:29:01 +00:00
renovate[bot] 906390f0bf Update shogo82148/actions-upload-release-asset digest to 8482bd7 (#4516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:10:31 +00:00
renovate[bot] 795497fafa Update crazy-max/ghaction-import-gpg digest to cb9bde2 (#4515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:10:01 +00:00
renovate[bot] ffb777d118 Update typescript-eslint monorepo to v8.13.0 (#4521)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:08:01 +00:00
renovate[bot] fbba8a2d71 Update typedoc (#4518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:08:00 +00:00
renovate[bot] 053c5741b0 Update actions/checkout digest to 11bd719 (#4514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:07:59 +00:00
169 changed files with 7664 additions and 4653 deletions
+1
View File
@@ -138,6 +138,7 @@ module.exports = {
tryExtensions: [".ts"],
},
],
"no-extra-boolean-cast": "error",
},
},
{
+4 -4
View File
@@ -2,7 +2,7 @@
## Checklist
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+2
View File
@@ -7,6 +7,8 @@ on:
branches:
- develop
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
backport:
name: Backport
+5 -2
View File
@@ -5,16 +5,19 @@ on:
workflows: ["Static Analysis"]
types:
- completed
permissions: {}
jobs:
netlify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-24.04
permissions:
actions: read
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: docs
path: docs
@@ -13,7 +13,7 @@ on:
#
#push:
# branches: [develop, master]
permissions: {} # No permissions required
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
+1
View File
@@ -3,6 +3,7 @@ on:
push:
branches: [develop]
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks
+4 -1
View File
@@ -9,12 +9,13 @@ on:
ELEMENT_BOT_TOKEN:
required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
changelog:
name: Preview Changelog
runs-on: ubuntu-24.04
steps:
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
- uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -44,6 +45,8 @@ jobs:
name: Label Community PRs
runs-on: ubuntu-24.04
if: github.event.action == 'opened'
permissions:
pull-requests: write
steps:
- name: Check membership
if: github.event.pull_request.user.login != 'renovate[bot]'
+38
View File
@@ -0,0 +1,38 @@
name: Release Sanity checks
on:
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: false
inputs:
repository:
type: string
required: false
default: ${{ github.repository }}
description: "The repository (in form owner/repo) to check for release blockers"
permissions: {}
jobs:
checks:
name: Sanity checks
runs-on: ubuntu-24.04
steps:
- name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@v7
env:
REPO: ${{ inputs.repository }}
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const { REPO } = process.env;
const { data } = await github.rest.search.issuesAndPullRequests({
q: `repo:${REPO} label:X-Release-Blocker is:open`,
per_page: 50,
});
if (data.total_count) {
data.items.forEach(item => {
core.error(`Release blocker: ${item.html_url}`);
});
core.setFailed(`Found release blockers!`);
}
@@ -8,9 +8,12 @@ on:
type: string
required: false
concurrency: release-drafter-action
permissions: {}
jobs:
draft:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
+3
View File
@@ -8,6 +8,9 @@ on:
branches: [staging]
workflow_dispatch: {}
concurrency: ${{ github.workflow }}
permissions: {}
jobs:
draft:
permissions:
contents: write
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
+2
View File
@@ -13,12 +13,14 @@ on:
type: string
required: false
concurrency: ${{ github.workflow }}
permissions: {} # Uses ELEMENT_BOT_TOKEN
jobs:
merge:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
+19 -36
View File
@@ -20,10 +20,6 @@ on:
description: Publish to npm
type: boolean
default: false
downstreams:
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
type: string
required: false
gpg-fingerprint:
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
type: string
@@ -38,16 +34,31 @@ on:
description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number
required: false
outputs:
npm-id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
permissions: {}
jobs:
checks:
name: Sanity checks
permissions:
issues: read
pull-requests: read
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
release:
name: Release
runs-on: ubuntu-24.04
environment: Release
needs: checks
permissions:
contents: write
steps:
- name: Load GPG key
id: gpg
if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -65,6 +76,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: staging
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
@@ -274,6 +286,8 @@ jobs:
name: Post release steps
needs: release
runs-on: ubuntu-24.04
permissions:
issues: write
steps:
- id: repository
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
@@ -297,34 +311,3 @@ jobs:
# wait-interval: 10
# check-name: merge
# allowed-conclusions: success
bump-downstreams:
name: Update npm dependency in downstream projects
needs: npm
runs-on: ubuntu-24.04
if: inputs.downstreams
strategy:
matrix:
repo: ${{ fromJSON(inputs.downstreams) }}
steps:
- uses: actions/checkout@v4
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.npm.outputs.id }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
+1
View File
@@ -8,6 +8,7 @@ on:
id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
permissions: {} # No permissions required
jobs:
npm:
name: Publish to npm
+36 -1
View File
@@ -21,14 +21,49 @@ on:
type: boolean
default: true
concurrency: ${{ github.workflow }}
permissions: {} # No permissions required
jobs:
release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
permissions:
contents: write
issues: write
pull-requests: read
secrets: inherit
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
downstreams: '["element-hq/element-web"]'
bump-downstreams:
name: Update npm dependency in downstream projects
needs: release
runs-on: ubuntu-24.04
strategy:
matrix:
repo:
- element-hq/element-web
steps:
- uses: actions/checkout@v4
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
docs:
name: Publish Documentation
+19 -7
View File
@@ -5,23 +5,29 @@ on:
secrets:
SONAR_TOKEN:
required: true
# No longer used
ELEMENT_BOT_TOKEN:
required: true
required: false
inputs:
sharded:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
permissions: {}
jobs:
sonarqube:
runs-on: ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'merge_group'
permissions:
actions: read
statuses: write
id-token: write # sonar
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
- uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -30,7 +36,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
@@ -40,7 +46,7 @@ jobs:
uses: actions/download-artifact@v4
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: coverage
path: coverage
@@ -48,17 +54,23 @@ jobs:
uses: actions/download-artifact@v4
if: inputs.sharded
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: coverage-*
path: coverage
merge-multiple: true
- name: Check coverage artifact
run: |
if [ ! -d coverage ]; then
echo "Coverage not found. Exiting with failure."
exit 1
fi
- id: extra_args
run: |
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
reports=$(find coverage -type f -name '*sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
- name: "🩻 SonarCloud Scan"
@@ -75,7 +87,7 @@ jobs:
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
- uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+5
View File
@@ -7,10 +7,15 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
permissions: {}
jobs:
sonarqube:
name: 🩻 SonarQube
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
permissions:
actions: read
statuses: write
id-token: write # sonar
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+70
View File
@@ -8,6 +8,7 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {} # No permissions needed
jobs:
ts_lint:
name: "Typescript Syntax Check"
@@ -43,6 +44,41 @@ jobs:
- name: Run Linter
run: "yarn run lint:js"
node_example_lint:
name: "Node.js example"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
- name: Build Types
run: "yarn build:types"
- uses: actions/setup-node@v4
with:
cache: "npm"
node-version-file: "examples/node/package.json"
# cache-dependency-path: '**/package-lock.json'
- name: Install Example Deps
run: "npm install"
working-directory: "examples/node"
- name: Check Syntax
run: "node --check app.js"
working-directory: "examples/node"
- name: Typecheck
run: "npx tsc"
working-directory: "examples/node"
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-24.04
@@ -101,3 +137,37 @@ jobs:
- name: Run linter
run: "yarn run lint:knip"
element-web:
name: Downstream tsc element-web
if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
repository: element-hq/element-web
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Install Dependencies
run: "./scripts/layered.sh"
env:
# tell layered.sh to check out the right sha of the JS-SDK
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
- name: Typecheck
run: "yarn run lint:types"
# Hook for branch protection to skip downstream typechecking outside of merge queues
downstream:
name: Downstream Typescript Syntax Check
runs-on: ubuntu-24.04
if: always()
needs:
- element-web
steps:
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
run: exit 1
+1
View File
@@ -8,6 +8,7 @@ on:
- develop
paths:
- .github/labels.yml
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
+7 -1
View File
@@ -10,6 +10,7 @@ concurrency:
cancel-in-progress: true
env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
permissions: {} # No permissions required
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
@@ -77,6 +78,8 @@ jobs:
name: Downstream test element-web
if: github.event_name == 'merge_group'
uses: element-hq/element-web/.github/workflows/tests.yml@develop
permissions:
statuses: write
with:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
@@ -84,6 +87,7 @@ jobs:
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
permissions: read-all
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
with:
use_js_sdk: "."
@@ -107,10 +111,12 @@ jobs:
if: always()
needs:
- element-web
permissions:
statuses: write
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+1 -1
View File
@@ -3,7 +3,7 @@ name: Move new issues into Issue triage board
on:
issues:
types: [opened]
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
automate-project-columns-next:
runs-on: ubuntu-24.04
+1 -1
View File
@@ -3,7 +3,7 @@ name: Move labelled issues to correct projects
on:
issues:
types: [labeled]
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
call-triage-labelled:
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
+97
View File
@@ -1,3 +1,100 @@
Changes in [36.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.1.0) (2025-01-28)
==================================================================================================
## ✨ Features
* Deprecate `MatrixClient.login` and replace with `loginRequest` ([#4632](https://github.com/matrix-org/matrix-js-sdk/pull/4632)). Contributed by @richvdh.
* Use `SyncCryptoCallback` api instead of legacy crypto in sliding sync ([#4624](https://github.com/matrix-org/matrix-js-sdk/pull/4624)). Contributed by @florianduros.
* Distinguish room state and timeline events in embedded clients ([#4574](https://github.com/matrix-org/matrix-js-sdk/pull/4574)). Contributed by @robintown.
* Allow setting default secret storage key id to null ([#4615](https://github.com/matrix-org/matrix-js-sdk/pull/4615)). Contributed by @florianduros.
* Add authenticated media to getAvatarUrl in room and room-member models ([#4616](https://github.com/matrix-org/matrix-js-sdk/pull/4616)). Contributed by @m004.
* Send MSC3981 'recurse' param on `/relations` endpoint on Matrix 1.10 servers ([#4023](https://github.com/matrix-org/matrix-js-sdk/pull/4023)). Contributed by @dbkr.
## 🐛 Bug Fixes
* [Backport staging] Revert "Distinguish room state and timeline events in embedded clients (#4574)" ([#4657](https://github.com/matrix-org/matrix-js-sdk/pull/4657)). Contributed by @RiotRobot.
* Change randomString et al to be secure ([#4621](https://github.com/matrix-org/matrix-js-sdk/pull/4621)). Contributed by @dbkr.
* Fix issue with sentinels being incorrect on m.room.member events ([#4609](https://github.com/matrix-org/matrix-js-sdk/pull/4609)). Contributed by @t3chguy.
Changes in [36.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.0.0) (2025-01-14)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership ([#4583](https://github.com/matrix-org/matrix-js-sdk/pull/4583)). Contributed by @toger5.
## ✨ Features
* MatrixRTC: Implement expiry logic for CallMembership and additional test coverage ([#4587](https://github.com/matrix-org/matrix-js-sdk/pull/4587)). Contributed by @toger5.
## 🐛 Bug Fixes
* Don't retry on 4xx responses ([#4601](https://github.com/matrix-org/matrix-js-sdk/pull/4601)). Contributed by @dbkr.
* Upgrade matrix-sdk-crypto-wasm to 12.1.0 ([#4596](https://github.com/matrix-org/matrix-js-sdk/pull/4596)). Contributed by @andybalaam.
* Avoid key prompts when resetting crypto ([#4586](https://github.com/matrix-org/matrix-js-sdk/pull/4586)). Contributed by @dbkr.
* Handle when aud OIDC claim is an Array ([#4584](https://github.com/matrix-org/matrix-js-sdk/pull/4584)). Contributed by @liamdiprose.
* Save the key backup key to 4S during `bootstrapSecretStorage ` ([#4542](https://github.com/matrix-org/matrix-js-sdk/pull/4542)). Contributed by @dbkr.
* Only re-prepare MatrixrRTC delayed disconnection event on 404 ([#4575](https://github.com/matrix-org/matrix-js-sdk/pull/4575)). Contributed by @toger5.
Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18)
==================================================================================================
This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage.
## 🐛 Bug Fixes
* Upgrade matrix-sdk-crypto-wasm to 1.11.0 ([#4593](https://github.com/matrix-org/matrix-js-sdk/pull/4593)).
Changes in [35.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.0.0) (2024-12-17)
==================================================================================================
## 🚨 BREAKING CHANGES
This release contains several breaking changes which will need code changes in your app. Most notably, `initCrypto()`
no longer exists and has been moved to `initLegacyCrypto()` in preparation for the eventual removal of Olm. You can
continue to use legacy Olm crypto for now by calling `initLegacyCrypto()` instead.
You may also need to make further changes if you use more advanced APIs. See the individual PRs (listed in order of size of change) for specific APIs changed and how to migrate.
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
## 🦖 Deprecations
* Deprecate remaining legacy functions and move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler ([#4560](https://github.com/matrix-org/matrix-js-sdk/pull/4560)). Contributed by @florianduros.
## ✨ Features
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
* Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress ([#4561](https://github.com/matrix-org/matrix-js-sdk/pull/4561)). Contributed by @hughns.
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" ([#4532](https://github.com/matrix-org/matrix-js-sdk/pull/4532)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix age field check in event echo processing ([#3635](https://github.com/matrix-org/matrix-js-sdk/pull/3635)). Contributed by @stas-demydiuk.
Changes in [34.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.13.0) (2024-12-03)
====================================================================================================
## 🦖 Deprecations
* Deprecate `MatrixClient.isEventSenderVerified` ([#4527](https://github.com/matrix-org/matrix-js-sdk/pull/4527)). Contributed by @florianduros.
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
## ✨ Features
* Ensure we disambiguate display names which look like MXIDs ([#4540](https://github.com/matrix-org/matrix-js-sdk/pull/4540)). Contributed by @t3chguy.
* Add `CryptoApi.getBackupInfo` ([#4512](https://github.com/matrix-org/matrix-js-sdk/pull/4512)). Contributed by @florianduros.
* Fix local echo in embedded mode ([#4498](https://github.com/matrix-org/matrix-js-sdk/pull/4498)). Contributed by @toger5.
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix `RustBackupManager` remaining values after current backup removal ([#4537](https://github.com/matrix-org/matrix-js-sdk/pull/4537)). Contributed by @florianduros.
Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19)
====================================================================================================
## 🦖 Deprecations
+136 -49
View File
@@ -38,8 +38,8 @@ client.publicRooms(function (err, data) {
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node/README.md) for a more complex example.
See [below](#end-to-end-encryption-support) for how to enable end-to-end-encryption, or check
[the Node.js terminal app](https://github.com/matrix-org/matrix-js-sdk/tree/develop/examples/node) for a more complex example.
To start the client:
@@ -126,26 +126,26 @@ const img = await fetch(downloadUrl, {
This SDK provides a full object model around the Matrix Client-Server API and emits
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
- Handles syncing (via `/sync`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
- Handles syncing (via `/sync`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
# Usage
@@ -303,44 +303,131 @@ Then visit `http://localhost:8005` to see the API docs.
# End-to-end encryption support
**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support
from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).
`matrix-js-sdk`'s end-to-end encryption support is based on the [WebAssembly bindings](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm) of the Rust [matrix-sdk-crypto](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto) library.
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the `Olm` global.
## Initialization
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
initialise the crypto layer.
**Do not use `matrixClient.initLegacyCrypto()`. This method is deprecated and no longer maintained.**
If the `Olm` global is not available, the SDK will show a warning, as shown
below; `initCrypto()` will also fail.
To initialize the end-to-end encryption support in the matrix client:
```
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
```javascript
// Create a new matrix client
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
// Initialize to enable end-to-end encryption support.
await matrixClient.initRustCrypto();
```
If the crypto layer is not (successfully) initialised, the SDK will continue to
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
specification.
After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto).
To provide the Olm library in a browser application:
**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time.
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
## Secret storage
To provide the Olm library in a node.js application:
You should normally set up [secret storage](https://spec.matrix.org/v1.12/client-server-api/#secret-storage) before using the end-to-end encryption. To do this, call [`CryptoApi.bootstrapSecretStorage`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapSecretStorage).
`bootstrapSecretStorage` can be called unconditionally: it will only set up the secret storage if it is not already set up (unless you use the `setupNewSecretStorage` parameter).
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
(replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/)
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
```javascript
const matrixClient = sdk.createClient({
...,
cryptoCallbacks: {
getSecretStorageKey: async (keys) => {
// This function should prompt the user to enter their secret storage key.
return mySecretStorageKeys;
},
},
});
If you want to package Olm as dependency for your node.js application, you can
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
application also works without e2e crypto enabled, add `--optional` to mark it
as an optional dependency.
matrixClient.getCrypto().bootstrapSecretStorage({
// This function will be called if a new secret storage key (aka recovery key) is needed.
// You should prompt the user to save the key somewhere, because they will need it to unlock secret storage in future.
createSecretStorageKey: async () => {
return mySecretStorageKey;
},
});
```
The example above will create a new secret storage key if secret storage was not previously set up.
The secret storage data will be encrypted using the secret storage key returned in [`createSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CreateSecretStorageOpts.html#createSecretStorageKey).
We recommend that you prompt the user to re-enter this key when [`CryptoCallbacks.getSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) is called (when the secret storage access is needed).
## Set up cross-signing
To set up cross-signing to verify devices and other users, call
[`CryptoApi.bootstrapCrossSigning`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapCrossSigning):
```javascript
matrixClient.getCrypto().bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
return makeRequest(authDict);
},
});
```
The [`authUploadDeviceSigningKeys`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.BootstrapCrossSigningOpts.html#authUploadDeviceSigningKeys)
callback is required in order to upload newly-generated public cross-signing keys to the server.
## Key backup
If the user doesn't already have a [key backup](https://spec.matrix.org/v1.12/client-server-api/#server-side-key-backups) you should create one:
```javascript
// Check if we have a key backup.
// If checkKeyBackupAndEnable returns null, there is no key backup.
const hasKeyBackup = (await matrixClient.getCrypto().checkKeyBackupAndEnable()) !== null;
// Create the key backup
await matrixClient.getCrypto().resetKeyBackup();
```
## Verify a new device
Once the cross-signing is set up on one of your devices, you can verify another device with two methods:
1. Use `CryptoApi.bootstrapCrossSigning`.
`bootstrapCrossSigning` will call the [CryptoCallbacks.getSecretStorageKey](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) callback. The device is verified with the private cross-signing keys fetched from the secret storage.
2. Request an interactive verification against existing devices, by calling [CryptoApi.requestOwnUserVerification](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#requestOwnUserVerification).
## Migrating from the legacy crypto stack to Rust crypto
If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initLegacyCrypto()`), you will
need to migrate existing devices to the Rust crypto stack.
This migration happens automatically when you call `initRustCrypto()` instead of `initLegacyCrypto()`,
but you need to provide the legacy [`cryptoStore`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#cryptoStore) and [`pickleKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#pickleKey) to [`createClient`](https://matrix-org.github.io/matrix-js-sdk/functions/matrix.createClient.html):
```javascript
// You should provide the legacy crypto store and the pickle key to the matrix client in order to migrate the data.
const matrixClient = sdk.createClient({
cryptoStore: myCryptoStore,
pickleKey: myPickleKey,
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
// The migration will be done automatically when you call `initRustCrypto`.
await matrixClient.initRustCrypto();
```
To follow the migration progress, you can listen to the [`CryptoEvent.LegacyCryptoStoreMigrationProgress`](https://matrix-org.github.io/matrix-js-sdk/enums/crypto_api.CryptoEvent.html#LegacyCryptoStoreMigrationProgress) event:
```javascript
// When progress === total === -1, the migration is finished.
matrixClient.on(CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress, total) => {
...
});
```
The Rust crypto stack is not supported in a lot of deprecated methods of [`MatrixClient`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html). If you use them, you should migrate to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html). Also, the legacy `MatrixClient.crypto` object is not available any more: you should use `MatrixClient.getCrypto()` instead.
# Contributing
+3 -3
View File
@@ -1,8 +1,8 @@
# Summary
- [Introduction](../README.md)
- [Introduction](../README.md)
# Deep dive
- [Storage notes](storage-notes.md)
- [Unverified devices](warning-on-unverified-devices.md)
- [Storage notes](storage-notes.md)
- [Unverified devices](warning-on-unverified-devices.md)
+27 -27
View File
@@ -20,19 +20,19 @@ blurrier.
When we are low on disk space overall or near the group limit / origin quota:
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
## Cache Eviction
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
separately from the others, so it has a separate quota limit and isn't evicted
when low on space.
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
## Persistent Storage
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
request persistent storage that won't be deleted by the browser because of low
space.
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains.
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
+7 -7
View File
@@ -17,13 +17,13 @@ Warn when you initial sync if the room has any new undefined devices since you w
Warn when the user tries to send a message:
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
+9 -15
View File
@@ -94,20 +94,14 @@ rl.on("line", function (line) {
);
} else if (line.indexOf("/file ") === 0) {
var filename = line.split(" ")[1].trim();
var stream = fs.createReadStream(filename);
matrixClient
.uploadContent({
stream: stream,
name: filename,
})
.then(function (url) {
var content = {
msgtype: MsgType.File,
body: filename,
url: JSON.parse(url).content_uri,
};
matrixClient.sendMessage(viewingRoom.roomId, content);
let buffer = fs.readFileSync("./your_file_name");
matrixClient.uploadContent(new Blob([buffer])).then(function (response) {
matrixClient.sendMessage(viewingRoom.roomId, {
msgtype: MsgType.File,
body: filename,
url: response.content_uri,
});
});
} else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
printMessages();
@@ -167,7 +161,7 @@ matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
if (!viewingRoom || viewingRoom.roomId !== room.roomId) {
if (!viewingRoom || viewingRoom.roomId !== room?.roomId) {
return; // not viewing a room or viewing the wrong room.
}
printLine(event);
@@ -386,7 +380,7 @@ function print(str, formatter) {
}
console.log.apply(console.log, newArgs);
} else {
console.log.apply(console.log, arguments);
console.log.apply(console.log, [...arguments]);
}
}
+7
View File
@@ -9,5 +9,12 @@
"dependencies": {
"cli-color": "^1.0.0",
"matrix-js-sdk": "^34.5.0"
},
"devDependencies": {
"@types/cli-color": "^2.0.6",
"typescript": "^5.6.2"
},
"engines": {
"node": ">=20.0.0"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"esModuleInterop": true,
"noImplicitAny": false,
"noEmit": true,
"skipLibCheck": true,
"allowJs": true,
"checkJs": true,
"strict": true
},
"include": ["app.js"]
}
+15 -3
View File
@@ -6,11 +6,21 @@ export default {
"src/types.ts",
"src/browser-index.ts",
"src/indexeddb-worker.ts",
"src/crypto-api/index.ts",
"src/testing.ts",
"src/matrix.ts",
"scripts/**",
"spec/**",
"release.sh",
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
"src/**",
// XXX: these look entirely unused
"src/crypto/aes.ts",
"src/crypto/crypto.ts",
"src/crypto/recoverykey.ts",
// XXX: these should be re-exported by one of the supported exports
"src/matrixrtc/index.ts",
"src/sliding-sync.ts",
"src/webrtc/groupCall.ts",
"src/webrtc/stats/media/mediaTrackStats.ts",
"src/rendezvous/RendezvousChannel.ts",
],
project: ["**/*.{js,ts}"],
ignore: ["examples/**"],
@@ -33,4 +43,6 @@ export default {
"dist",
],
ignoreExportsUsedInFile: true,
includeEntryExports: false,
exclude: ["enumMembers"],
} satisfies KnipConfig;
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.12.0",
"version": "36.2.0-rc.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^13.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
@@ -103,7 +103,7 @@
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "^2.0.1",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.5",
@@ -117,12 +117,12 @@
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.3.3",
"prettier": "3.4.2",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.26.0",
"typedoc": "^0.27.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-mdn-links": "^4.0.0",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.4.2"
},
+33 -7
View File
@@ -1327,7 +1327,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const syncResponse = getSyncResponse(["@bob:xyz"]);
// Every 2 messages in the room, the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2",
rotation_period_msgs: 2,
};
@@ -1383,7 +1383,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const oneHourInMs = 60 * 60 * 1000;
// Every 1h the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2",
rotation_period_ms: oneHourInMs,
};
@@ -1741,7 +1741,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
groupSession: groupSession,
room_id: ROOM_ID,
});
await testClient.client.initCrypto();
await testClient.client.initLegacyCrypto();
const keys = [
{
room_id: ROOM_ID,
@@ -1853,7 +1853,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
oldBackendOnly("Alice receives shared history before being invited to a room by the sharer", async () => {
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
await beccaTestClient.client.initCrypto();
await beccaTestClient.client.initLegacyCrypto();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -2007,7 +2007,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
oldBackendOnly("Alice receives shared history before being invited to a room by someone else", async () => {
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
await beccaTestClient.client.initCrypto();
await beccaTestClient.client.initLegacyCrypto();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -3121,6 +3121,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
});
newBackendOnly("should upload existing megolm backup key to a new 4S store", async () => {
const backupKeyTo4SPromise = awaitMegolmBackupKeyUpload();
// we need these to set up the mocks but we don't actually care whether they
// resolve because we're not testing those things in this test.
awaitCrossSigningKeyUpload("master");
awaitCrossSigningKeyUpload("user_signing");
awaitCrossSigningKeyUpload("self_signing");
awaitSecretStorageKeyStoredInAccountData();
mockSetupCrossSigningRequests();
mockSetupMegolmBackupRequests("1");
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
await aliceClient.getCrypto()!.resetKeyBackup();
await aliceClient.getCrypto()!.bootstrapSecretStorage({
setupNewSecretStorage: true,
createSecretStorageKey,
setupNewKeyBackup: false,
});
await backupKeyTo4SPromise;
expect(accountDataAccumulator.accountDataEvents.get("m.megolm_backup.v1")).toBeDefined();
});
});
describe("Manage Key Backup", () => {
@@ -3142,7 +3168,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
resolve(uploadPayload);
return {
status: 200,
@@ -3209,7 +3235,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
backupData.version = newVersion;
backupData.count = 0;
backupData.etag = "zer";
+47 -1
View File
@@ -17,11 +17,13 @@ limitations under the License.
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
import { ClientEvent, createClient, MatrixClient, MatrixEvent } from "../../../src";
import { CryptoEvent } from "../../../src/crypto-api/index";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { emitPromise, EventCounter } from "../../test-utils/test-utils";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
@@ -40,6 +42,12 @@ describe("Device dehydration", () => {
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
// count the number of times the dehydration key gets set
let setDehydrationCount = 0;
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
@@ -74,6 +82,8 @@ describe("Device dehydration", () => {
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
expect(creationEventCounter.counter).toEqual(1);
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
// a week later, we should have created another dehydrated device
const dehydrationPromise = new Promise<void>((resolve, reject) => {
@@ -81,7 +91,10 @@ describe("Device dehydration", () => {
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
expect(dehydrationCount).toEqual(2);
expect(creationEventCounter.counter).toEqual(2);
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
@@ -113,6 +126,39 @@ describe("Device dehydration", () => {
expect(setDehydrationCount).toEqual(2);
expect(eventsResponse.mock.calls).toHaveLength(2);
expect(rehydrationStartedCounter.counter).toEqual(1);
expect(rehydrationCompletedCounter.counter).toEqual(1);
expect(creationEventCounter.counter).toEqual(3);
expect(rehydrationProgressCounter.counter).toEqual(1);
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
// test that if we get an error when we try to rotate, it emits an event
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
});
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await rotationErrorEventPromise;
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
});
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
return {};
});
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
await crypto.startDehydration(true);
await rehydrationErrorEventPromise;
matrixClient.stopClient();
});
});
+159 -47
View File
@@ -22,7 +22,7 @@ import { Mocked } from "jest-mock";
import {
createClient,
Crypto,
CryptoEvent,
encodeBase64,
ICreateClientOpts,
IEvent,
IMegolmSessionData,
@@ -44,7 +44,7 @@ import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { flushPromises } from "../../test-utils/flushPromises";
import { defer, IDeferred } from "../../../src/utils";
import { DecryptionFailureCode } from "../../../src/crypto-api";
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent } from "../../../src/crypto-api";
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -91,7 +91,7 @@ function mockUploadEmitter(
},
};
}
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
@@ -117,8 +117,10 @@ function mockUploadEmitter(
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
// const newBackendOnly = backend === "libolm" ? test.skip : test;
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend === "libolm" ? test.skip : test;
const isNewBackend = backend === "rust-sdk";
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
@@ -247,9 +249,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// On the first decryption attempt, decryption fails.
await awaitDecryption(event);
expect(event.decryptionFailureReason).toEqual(
backend === "libolm"
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
isNewBackend
? DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP
: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
// Eventually, decryption succeeds.
@@ -314,6 +316,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.get(
`path:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`,
testData.SIGNED_BACKUP_DATA,
);
aliceClient = await initTestClient();
aliceCrypto = aliceClient.getCrypto()!;
@@ -344,20 +350,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
onKeyCached = resolve;
});
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const result = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
isNewBackend
? aliceCrypto.restoreKeyBackup()
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
);
expect(result.imported).toStrictEqual(1);
if (isNewBackend) return;
await awaitKeyCached;
// The key should be now cached
@@ -398,8 +413,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
it("Should import full backup in chunks", async function () {
const importMockImpl = jest.fn();
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}
// We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
@@ -408,17 +428,26 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const check = await aliceCrypto.checkKeyBackupAndEnable();
const progressCallback = jest.fn();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup({
progressCallback,
})
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));
expect(result.imported).toStrictEqual(expectedTotal);
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
expect(importMockImpl).toHaveBeenCalledTimes(5);
@@ -451,8 +480,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
it("Should continue to process backup if a chunk import fails and report failures", async function () {
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = jest
const importMockImpl = jest
.fn()
.mockImplementationOnce(() => {
// Fail to import first chunk
@@ -461,22 +489,36 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// Ok for other chunks
.mockResolvedValue(undefined);
if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
const check = await aliceCrypto.checkKeyBackupAndEnable();
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup({ progressCallback })
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));
expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
@@ -527,20 +569,26 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
const check = await aliceCrypto.checkKeyBackupAndEnable();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup()
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
));
expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
});
it("recover specific session from backup", async function () {
oldBackendOnly("recover specific session from backup", async function () {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
@@ -560,7 +608,33 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(result.imported).toStrictEqual(1);
});
it("Fails on bad recovery key", async function () {
newBackendOnly(
"Should get the decryption key from the secret storage and restore the key backup",
async function () {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);
const result = await aliceCrypto.restoreKeyBackup();
expect(result.imported).toStrictEqual(1);
},
);
oldBackendOnly("Fails on bad recovery key", async function () {
const fullBackup = {
rooms: {
[ROOM_ID]: {
@@ -584,6 +658,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
),
).rejects.toThrow();
});
newBackendOnly("Should throw an error if the decryption key is not found in cache", async () => {
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
});
});
describe("backupLoop", () => {
@@ -890,6 +968,40 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
});
newBackendOnly("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceCrypto.checkKeyBackupAndEnable();
// At this point there is no backup
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
// Return now the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
// Delete the backup and we are expecting the key backup to be disabled
const keyBackupStatus = defer<boolean>();
aliceClient.once(CryptoEvent.KeyBackupStatus, (enabled) => keyBackupStatus.resolve(enabled));
await aliceCrypto.deleteKeyBackupVersion(testData.SIGNED_BACKUP_DATA.version!);
expect(await keyBackupStatus.promise).toBe(false);
// The backup info should not be available anymore
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
});
describe("isKeyBackupTrusted", () => {
it("does not trust a backup signed by an untrusted device", async () => {
aliceClient = await initTestClient();
+2 -2
View File
@@ -345,10 +345,10 @@ describe("MatrixClient crypto", () => {
beforeEach(async () => {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
await aliTestClient.client.initLegacyCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
await bobTestClient.client.initLegacyCrypto();
aliMessages = [];
bobMessages = [];
+30 -15
View File
@@ -78,6 +78,7 @@ import {
encryptGroupSessionKey,
encryptMegolmEvent,
encryptSecretSend,
getTestOlmAccountKeys,
ToDeviceEvent,
} from "./olm-utils";
import { KeyBackupInfo } from "../../../src/crypto-api";
@@ -473,21 +474,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.phase).toEqual(VerificationPhase.Ready);
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const rawQrCodeBuffer = (await request.generateQRCode())!;
expect(rawQrCodeBuffer).toBeTruthy();
const qrCodeBuffer = new Uint8Array(rawQrCodeBuffer);
const textDecoder = new TextDecoder();
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
const txnIdLen = qrCodeBuffer.readUint16BE(8);
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
expect(textDecoder.decode(qrCodeBuffer.slice(0, 6))).toEqual("MATRIX");
expect(qrCodeBuffer[6]).toEqual(0x02); // version
expect(qrCodeBuffer[7]).toEqual(0x02); // mode
const txnIdLen = (qrCodeBuffer[8] << 8) + qrCodeBuffer[9];
expect(textDecoder.decode(qrCodeBuffer.slice(10, 10 + txnIdLen))).toEqual(transactionId);
// Alice's device's public key comes next, but we have nothing to do with it here.
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
// const aliceDevicePubKey = qrCodeBuffer.slice(10 + txnIdLen, 32 + 10 + txnIdLen);
expect(encodeUnpaddedBase64(qrCodeBuffer.slice(42 + txnIdLen, 32 + 42 + txnIdLen))).toEqual(
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
);
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
const sharedSecret = qrCodeBuffer.slice(74 + txnIdLen);
// we should still be "Ready" and have no verifier
expect(request.phase).toEqual(VerificationPhase.Ready);
@@ -805,7 +808,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
const sharedSecret = qrCodeBuffer.slice(74 + transactionId.length);
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
@@ -990,6 +993,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
aliceClient.setGlobalErrorOnUnknownDevices(false);
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
await syncPromise(aliceClient);
// Rust crypto requires the sender's device keys before it accepts a
// verification request.
if (backend === "rust-sdk") {
const crypto = aliceClient.getCrypto()!;
const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice");
e2eKeyResponder.addDeviceKeys(bobDeviceKeys);
syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } });
await syncPromise(aliceClient);
await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]);
}
});
/**
@@ -1627,7 +1642,7 @@ function buildReadyMessage(
}
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) {
return {
type: "m.key.verification.start",
content: {
@@ -1723,7 +1738,7 @@ function buildQRCode(
key2Base64: string,
sharedSecret: string,
mode = 0x02,
): Uint8Array {
): Uint8ClampedArray {
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
const qrCodeBuffer = Buffer.alloc(150); // oversize
@@ -1739,5 +1754,5 @@ function buildQRCode(
idx += qrCodeBuffer.write(sharedSecret, idx);
// truncate to the right length
return qrCodeBuffer.subarray(0, idx);
return new Uint8ClampedArray(qrCodeBuffer.subarray(0, idx));
}
+1 -1
View File
@@ -77,7 +77,7 @@ describe("DeviceList management:", function () {
async function createTestClient() {
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
await testClient.client.initCrypto();
await testClient.client.initLegacyCrypto();
return testClient;
}
@@ -1144,7 +1144,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend
.when(
"GET",
@@ -1155,7 +1155,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1247,7 +1247,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend
.when(
"GET",
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1560,7 +1560,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.NewReply);
respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(2);
@@ -1685,7 +1685,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.Update);
respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY_REACTION]);
await room.addLiveEvents([THREAD_REPLY_REACTION], { addToState: false });
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
+57 -26
View File
@@ -168,14 +168,17 @@ describe("MatrixClient", function () {
type: "test",
content: {},
});
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
@@ -188,14 +191,17 @@ describe("MatrixClient", function () {
const roomId = "!roomId:server";
const roomAlias = "#my-fancy-room:server";
const room = new Room(roomId, client, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
],
{ addToState: true },
);
store.storeRoom(room);
// The method makes a request to resolve the alias
@@ -275,14 +281,17 @@ describe("MatrixClient", function () {
content: {},
});
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Knock,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Knock,
event: true,
}),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
@@ -641,9 +650,9 @@ describe("MatrixClient", function () {
}
beforeEach(function () {
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
httpBackend.when("POST", "/keys/upload").respond(200, {});
return Promise.all([client.initCrypto(), httpBackend.flush("/keys/upload", 1)]);
return Promise.all([client.initLegacyCrypto(), httpBackend.flush("/keys/upload", 1)]);
});
afterEach(() => {
@@ -1906,6 +1915,28 @@ describe("MatrixClient", function () {
return prom;
});
});
describe("getDomain", () => {
it("should return null if no userId is set", () => {
const client = new MatrixClient({ baseUrl: "http://localhost" });
expect(client.getDomain()).toBeNull();
});
it("should return the domain of the userId", () => {
expect(client.getDomain()).toBe("localhost");
});
});
describe("getUserIdLocalpart", () => {
it("should return null if no userId is set", () => {
const client = new MatrixClient({ baseUrl: "http://localhost" });
expect(client.getUserIdLocalpart()).toBeNull();
});
it("should return the localpart of the userId", () => {
expect(client.getUserIdLocalpart()).toBe("alice");
});
});
});
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
+173 -7
View File
@@ -50,6 +50,13 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
a: {};
b: {};
}
}
describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
@@ -112,7 +119,7 @@ describe("MatrixClient syncing", () => {
});
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
await client!.initCrypto();
await client!.initRustCrypto();
const roomId = "!cycles:example.org";
@@ -227,7 +234,7 @@ describe("MatrixClient syncing", () => {
});
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
await client!.initCrypto();
await client!.initRustCrypto();
const roomId = "!cycles:example.org";
@@ -556,7 +563,7 @@ describe("MatrixClient syncing", () => {
});
it("should resolve incoming invites from /sync", () => {
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -589,7 +596,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost",
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -617,7 +624,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost",
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -644,7 +651,7 @@ describe("MatrixClient syncing", () => {
});
it("should no-op if resolveInvitesToProfiles is not set", () => {
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -1373,6 +1380,114 @@ describe("MatrixClient syncing", () => {
expect(stateEventEmitCount).toEqual(2);
});
});
describe("msc4222", () => {
const roomOneSyncOne = {
"timeline": {
events: [
utils.mkMessage({
room: roomOne,
user: otherUserId,
msg: "hello",
}),
],
},
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: otherUserId,
content: {
name: "Initial room name",
},
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {},
}),
],
},
};
const roomOneSyncTwo = {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
it("should ignore state events in timeline when state_after is present", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
});
});
describe("timeline", () => {
@@ -2274,6 +2389,57 @@ describe("MatrixClient syncing", () => {
}),
]);
});
describe("msc4222", () => {
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("POST", "/filter").respond(200, {
filter_id: "another_id",
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: {
[roomOne]: {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
},
},
},
});
const [[room]] = await Promise.all([
client!.syncLeftRooms(),
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend!.flush("/filter").then(() => {
return httpBackend!.flushAllExpected();
}),
]);
expect(room.name).toEqual("Empty room");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
});
describe("peek", () => {
@@ -2414,7 +2580,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
await idbClient.initCrypto();
await idbClient.initRustCrypto();
const roomId = "!invite:example.org";
@@ -128,7 +128,7 @@ describe("MatrixClient syncing", () => {
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
await room.addLiveEvents([thread.rootEvent]);
await room.addLiveEvents([thread.rootEvent], { addToState: false });
// Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
@@ -36,7 +36,7 @@ import {
MatrixError,
MatrixHttpApi,
} from "../../../src";
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
const baseUrl = "https://example.com";
@@ -57,7 +57,7 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
getDomain: () => "example.com",
getDevice: jest.fn(),
getCrypto: jest.fn(() => crypto),
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
} as unknown as MatrixClient;
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
baseUrl: client.baseUrl,
@@ -69,10 +69,6 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
describe("MSC4108SignInWithQR", () => {
beforeEach(() => {
fetchMock.get(
"https://issuer/.well-known/openid-configuration",
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
);
fetchMock.get("https://issuer/jwks", {
status: 200,
headers: {
+22 -21
View File
@@ -44,12 +44,21 @@ import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership";
import { SyncCryptoCallbacks } from "../../src/common-crypto/CryptoBackend";
declare module "../../src/@types/event" {
interface AccountDataEvents {
global_test: {};
tester: {};
}
}
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;
let sdk: SlidingSyncSdk | undefined;
let mockSlidingSync: SlidingSync | undefined;
let syncCryptoCallback: SyncCryptoCallbacks | undefined;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
@@ -119,8 +128,9 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
await client!.initRustCrypto({ useIndexedDB: false });
syncCryptoCallback = client!.getCrypto() as unknown as SyncCryptoCallbacks;
syncOpts.cryptoCallbacks = syncCryptoCallback;
}
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
@@ -601,13 +611,13 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true,
name: "Room with Invite",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
],
timeline: [],
});
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
@@ -633,13 +643,6 @@ describe("SlidingSyncSdk", () => {
ext = findExtension("e2ee");
});
afterAll(async () => {
// needed else we do some async operations in the background which can cause Jest to whine:
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
// Attempted to log "Saving device tracking data null"."
client!.crypto!.stop();
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
@@ -648,28 +651,28 @@ describe("SlidingSyncSdk", () => {
});
it("can update device lists", () => {
client!.crypto!.processDeviceLists = jest.fn();
syncCryptoCallback!.processDeviceLists = jest.fn();
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
},
});
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
expect(syncCryptoCallback!.processDeviceLists).toHaveBeenCalledWith({
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
});
});
it("can update OTK counts and unused fallback keys", () => {
client!.crypto!.processKeyCounts = jest.fn();
syncCryptoCallback!.processKeyCounts = jest.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
},
device_unused_fallback_key_types: ["signed_curve25519"],
});
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
expect(syncCryptoCallback!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
"signed_curve25519",
]);
});
@@ -921,13 +924,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true,
});
await emitPromise(client!, ClientEvent.Room);
@@ -962,13 +964,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true,
});
const room = client!.getRoom(roomId)!;
+1 -1
View File
@@ -88,7 +88,7 @@ export function mockSetupMegolmBackupRequests(backupVersion: string): void {
});
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
+1 -39
View File
@@ -14,42 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (
issuer = "https://auth.org/",
additionalGrantTypes: string[] = [],
): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
code_challenge_methods_supported: ["S256"],
});
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";
+15 -2
View File
@@ -86,7 +86,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
};
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
roomResponse.state!.events.push(
mkMembershipCustom({
membership: KnownMembership.Join,
sender: roomMembers[i],
@@ -561,11 +561,24 @@ export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (globalThis.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initLegacyCrypto();
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
/**
* Counts the number of times that an event was emitted.
*/
export class EventCounter {
public counter;
constructor(emitter: EventEmitter, event: string) {
this.counter = 0;
emitter.on(event, () => {
this.counter++;
});
}
}
/**
* Advance the fake timers in a loop until the given promise resolves or rejects.
*
+1 -1
View File
@@ -178,6 +178,6 @@ export const populateThread = ({
}: MakeThreadProps): MakeThreadResult => {
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
ret.thread.initialEventsFetched = true;
room.addLiveEvents(ret.events);
room.addLiveEvents(ret.events, { addToState: false });
return ret;
};
+3
View File
@@ -481,6 +481,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getUserId(): string {
return this.userId;
}
public getSafeUserId(): string {
return this.userId;
}
public getDeviceId(): string {
return this.deviceId;
+1 -25
View File
@@ -15,34 +15,10 @@ limitations under the License.
*/
import { TextEncoder, TextDecoder } from "util";
import NodeBuffer from "node:buffer";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64";
describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
let origBuffer = Buffer;
beforeAll(() => {
if (env === "browser") {
origBuffer = Buffer;
// @ts-ignore
// eslint-disable-next-line no-global-assign
Buffer = undefined;
globalThis.atob = NodeBuffer.atob;
globalThis.btoa = NodeBuffer.btoa;
}
});
afterAll(() => {
// eslint-disable-next-line no-global-assign
Buffer = origBuffer;
// @ts-ignore
globalThis.atob = undefined;
// @ts-ignore
globalThis.btoa = undefined;
});
describe("Base64 encoding", () => {
it("Should decode properly encoded data", () => {
const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ="));
+32
View File
@@ -207,6 +207,17 @@ describe("Topic content helpers", () => {
],
});
});
it("creates an empty event when the topic is falsey", () => {
expect(makeTopicContent(undefined)).toEqual({
topic: undefined,
[M_TOPIC.name]: [],
});
expect(makeTopicContent(null)).toEqual({
topic: null,
[M_TOPIC.name]: [],
});
});
});
describe("parseTopicContent()", () => {
@@ -257,5 +268,26 @@ describe("Topic content helpers", () => {
html: "<b>pizza</b>",
});
});
it("parses legacy event content", () => {
expect(
parseTopicContent({
topic: "pizza",
}),
).toEqual({
text: "pizza",
});
});
it("uses legacy event content when new topic key is invalid", () => {
expect(
parseTopicContent({
"topic": "pizza",
"m.topic": {} as any,
}),
).toEqual({
text: "pizza",
});
});
});
});
+16 -16
View File
@@ -119,7 +119,7 @@ describe("Crypto", function () {
it("getVersion() should return the current version of the olm library", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
const olmVersionTuple = Crypto.getOlmVersion();
expect(client.getCrypto()?.getVersion()).toBe(
@@ -130,7 +130,7 @@ describe("Crypto", function () {
describe("encrypted events", function () {
it("provides encryption information for events from unverified senders", async function () {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
// unencrypted event
const event = {
@@ -210,7 +210,7 @@ describe("Crypto", function () {
let client: MatrixClient;
beforeEach(async () => {
client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
// mock out the verification check
client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false);
@@ -306,7 +306,7 @@ describe("Crypto", function () {
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
const event = new MatrixEvent({
content: {},
@@ -439,10 +439,10 @@ describe("Crypto", function () {
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
claraClient = new TestClient("@clara:example.com", "claradevice").client;
await aliceClient.initCrypto();
await secondAliceClient.initCrypto();
await bobClient.initCrypto();
await claraClient.initCrypto();
await aliceClient.initLegacyCrypto();
await secondAliceClient.initLegacyCrypto();
await bobClient.initLegacyCrypto();
await claraClient.initLegacyCrypto();
});
afterEach(async function () {
@@ -1111,7 +1111,7 @@ describe("Crypto", function () {
jest.spyOn(logger, "debug").mockImplementation(() => {});
jest.setTimeout(10000);
const client = new TestClient("@a:example.com", "dev").client;
await client.initCrypto();
await client.initLegacyCrypto();
client.crypto!.isCrossSigningReady = async () => false;
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
@@ -1147,9 +1147,9 @@ describe("Crypto", function () {
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
@@ -1264,9 +1264,9 @@ describe("Crypto", function () {
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
@@ -1362,7 +1362,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async () => {
@@ -1388,7 +1388,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async () => {
@@ -1414,7 +1414,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async function () {
+10 -6
View File
@@ -610,7 +610,11 @@ describe("MegolmDecryption", function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
const bobClient2 = new TestClient("@bob:example.com", "bobdevice2").client;
await Promise.all([aliceClient.initCrypto(), bobClient1.initCrypto(), bobClient2.initCrypto()]);
await Promise.all([
aliceClient.initLegacyCrypto(),
bobClient1.initLegacyCrypto(),
bobClient2.initLegacyCrypto(),
]);
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice1 = bobClient1.crypto!.olmDevice;
const bobDevice2 = bobClient2.crypto!.olmDevice;
@@ -704,7 +708,7 @@ describe("MegolmDecryption", function () {
it("does not block unverified devices when sending verification events", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const bobDevice = bobClient.crypto!.olmDevice;
const encryptionCfg = {
@@ -789,7 +793,7 @@ describe("MegolmDecryption", function () {
it("notifies devices when unable to create olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice = bobClient.crypto!.olmDevice;
@@ -873,7 +877,7 @@ describe("MegolmDecryption", function () {
it("throws an error describing why it doesn't have a key", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const bobDevice = bobClient.crypto!.olmDevice;
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
@@ -955,7 +959,7 @@ describe("MegolmDecryption", function () {
it("throws an error describing the lack of an olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
@@ -1051,7 +1055,7 @@ describe("MegolmDecryption", function () {
it("throws an error to indicate a wedged olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
+15 -7
View File
@@ -231,7 +231,7 @@ describe("MegolmBackup", function () {
test("fail if given backup has no version", async () => {
const client = makeTestClient(cryptoStore);
await client.initCrypto();
await client.initLegacyCrypto();
const data = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: {
@@ -314,7 +314,7 @@ describe("MegolmBackup", function () {
megolmDecryption.olmlib = mockOlmLib;
return client
.initCrypto()
.initLegacyCrypto()
.then(() => {
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
@@ -391,7 +391,7 @@ describe("MegolmBackup", function () {
megolmDecryption.olmlib = mockOlmLib;
return client
.initCrypto()
.initLegacyCrypto()
.then(() => {
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
})
@@ -471,7 +471,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await client.initLegacyCrypto();
client.uploadDeviceSigningKeys = async function (e) {
return {};
};
@@ -560,7 +560,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await client.initLegacyCrypto();
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -636,7 +636,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
return client.initLegacyCrypto();
});
afterEach(function () {
@@ -773,11 +773,19 @@ describe("MegolmBackup", function () {
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
await client.initCrypto();
await client.initLegacyCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
client.stopClient();
});
});
describe("getKeyBackupInfo", () => {
it("should return throw an `Not implemented`", async () => {
const client = makeTestClient(cryptoStore);
await client.initLegacyCrypto();
await expect(client.getCrypto()?.getKeyBackupInfo()).rejects.toThrow("Not implemented");
});
});
});
+1 -1
View File
@@ -78,7 +78,7 @@ async function makeTestClient(
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
const client = testClient.client;
await client.initCrypto();
await client.initLegacyCrypto();
return { client, httpBackend: testClient.httpBackend };
}
+1 -1
View File
@@ -68,7 +68,7 @@ describe("Dehydration", () => {
},
});
await alice.client.initCrypto();
await alice.client.initLegacyCrypto();
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
errcode: "M_NOT_FOUND",
+8 -1
View File
@@ -30,6 +30,7 @@ import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64";
import { CrossSigningKeyInfo } from "../../../src/crypto-api";
import { SecretInfo } from "../../../src/secret-storage.ts";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@@ -43,7 +44,7 @@ async function makeTestClient(
return true;
};
await client.initCrypto();
await client.initLegacyCrypto();
// No need to download keys for these tests
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
@@ -68,6 +69,12 @@ function sign<T extends IObject | ICurve25519AuthData>(
};
}
declare module "../../../src/@types/event" {
interface SecretStorageAccountDataEvents {
foo: SecretInfo;
}
}
describe("Secrets", function () {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
+1 -1
View File
@@ -119,7 +119,7 @@ export async function makeTestClients(
clients.push(testClient);
}
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
await Promise.all(clients.map((testClient) => testClient.client.initLegacyCrypto()));
const destroy = () => {
timeouts.forEach((t) => clearTimeout(t));
+146 -1
View File
@@ -30,6 +30,7 @@ import {
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
ISendEventFromWidgetResponseData,
WidgetApiResponseError,
} from "matrix-widget-api";
@@ -40,6 +41,7 @@ import { ICapabilities, RoomWidgetClient } from "../../src/embedded";
import { MatrixEvent } from "../../src/models/event";
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { sleep } from "../../src/utils";
const testOIDCToken = {
access_token: "12345678",
@@ -127,9 +129,16 @@ describe("RoomWidgetClient", () => {
const makeClient = async (
capabilities: ICapabilities,
sendContentLoaded: boolean | undefined = undefined,
userId?: string,
): Promise<void> => {
const baseUrl = "https://example.org";
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }, sendContentLoaded);
client = createRoomWidgetClient(
widgetApi,
capabilities,
"!1:example.org",
{ baseUrl, userId },
sendContentLoaded,
);
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
widgetApi.emit("ready");
await client.startClient();
@@ -192,6 +201,142 @@ describe("RoomWidgetClient", () => {
.map((e) => e.getEffectiveEvent()),
).toEqual([event]);
});
describe("local echos", () => {
const setupRemoteEcho = () => {
makeClient(
{
receiveEvent: ["org.matrix.rageshake_request"],
sendEvent: ["org.matrix.rageshake_request"],
},
undefined,
"@me:example.org",
);
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const injectSpy = jest.spyOn((client as any).syncApi, "injectRoomEvents");
const widgetSendEmitter = new EventEmitter();
const widgetSendPromise = new Promise<void>((resolve) =>
widgetSendEmitter.once("send", () => resolve()),
);
const resolveWidgetSend = () => widgetSendEmitter.emit("send");
widgetApi.sendRoomEvent.mockImplementation(
async (eType, content, roomId): Promise<ISendEventFromWidgetResponseData> => {
await widgetSendPromise;
return { room_id: "!1:example.org", event_id: "event_id" };
},
);
return { injectSpy, resolveWidgetSend };
};
const remoteEchoEvent = new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
detail: {
data: {
type: "org.matrix.rageshake_request",
room_id: "!1:example.org",
event_id: "event_id",
sender: "@me:example.org",
state_key: "bar",
content: { hello: "world" },
unsigned: { transaction_id: "1234" },
},
},
cancelable: true,
});
it("get response then local echo", async () => {
await sleep(600);
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
// Begin by sending an event:
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
// we do not expect it to be send -- until we call `resolveWidgetSend`
expect(injectSpy).not.toHaveBeenCalled();
// We first get the response from the widget
resolveWidgetSend();
// We then get the remote echo from the widget
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
// gets emitted after the event got injected
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
});
it("get local echo then response", async () => {
await sleep(600);
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
// Begin by sending an event:
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
// we do not expect it to be send -- until we call `resolveWidgetSend`
expect(injectSpy).not.toHaveBeenCalled();
// We first get the remote echo from the widget
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
expect(injectSpy).not.toHaveBeenCalled();
// We then get the response from the widget
resolveWidgetSend();
// Gets emitted after the event got injected
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
});
it("__ local echo then response", async () => {
await sleep(600);
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
// Begin by sending an event:
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
// we do not expect it to be send -- until we call `resolveWidgetSend`
expect(injectSpy).not.toHaveBeenCalled();
// We first get the remote echo from the widget
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
const otherRemoteEcho = new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
detail: { data: { ...remoteEchoEvent.detail.data } },
});
otherRemoteEcho.detail.data.unsigned.transaction_id = "4567";
otherRemoteEcho.detail.data.event_id = "other_id";
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, otherRemoteEcho);
// Simulate the wait time while the widget is waiting for a response
// after we already received the remote echo
await sleep(20);
// even after the wait we do not want any event to be injected.
// we do not know their event_id and cannot know if they are the remote echo
// where we need to update the txId because they are send by this client
expect(injectSpy).not.toHaveBeenCalled();
// We then get the response from the widget
resolveWidgetSend();
// Gets emitted after the event got injected
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
// Now we want both events to be injected since we know the txId - event_id match
expect(injectSpy).toHaveBeenCalled();
// it has been called with the event sent by ourselves
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
// It has been called by the event we blocked because of our send right afterwards
const call2 = injectSpy.mock.calls[1] as any;
const injectedEv2 = call2[3][0];
expect(injectedEv2.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv2.getUnsigned().transaction_id).toBe("4567");
});
});
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to send");
+2 -2
View File
@@ -74,7 +74,7 @@ describe("eventMapperFor", function () {
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
@@ -109,7 +109,7 @@ describe("eventMapperFor", function () {
room.oldState.setStateEvents([event]);
room.currentState.setStateEvents([event]);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
+47 -5
View File
@@ -23,6 +23,7 @@ import {
EventTimelineSet,
EventType,
Filter,
KnownMembership,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
@@ -104,7 +105,7 @@ describe("EventTimelineSet", () => {
it("Adds event to the live timeline in the timeline set", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent);
eventTimelineSet.addLiveEvent(messageEvent, { addToState: false });
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
@@ -113,6 +114,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
@@ -130,12 +132,38 @@ describe("EventTimelineSet", () => {
// replace.
eventTimelineSet.addLiveEvent(duplicateMessageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
});
const eventsInLiveTimeline = liveTimeline.getEvents();
expect(eventsInLiveTimeline.length).toStrictEqual(1);
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
});
it("should set event.target after applying the membership state update", () => {
const eventTimelineSet = room.getUnfilteredTimelineSet();
const ev1 = utils.mkMembership({
room: roomId,
mship: KnownMembership.Invite,
user: "@sender:server",
skey: userA,
event: true,
});
const ev2 = utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: userA,
skey: userA,
name: "This is my displayname",
event: true,
});
eventTimelineSet.addLiveEvent(ev1, { addToState: true });
expect(ev1.target?.name).toBe(userA);
eventTimelineSet.addLiveEvent(ev2, { addToState: true });
expect(ev2.target?.name).toBe("This is my displayname");
});
});
describe("addEventToTimeline", () => {
@@ -144,6 +172,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
@@ -151,10 +180,17 @@ describe("EventTimelineSet", () => {
it("Make sure legacy overload passing options directly as parameters still works", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
}).not.toThrow();
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
fromCache: false,
addToState: false,
});
}).not.toThrow();
});
@@ -167,11 +203,13 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents()).toHaveLength(1);
const [event] = liveTimeline.getEvents();
@@ -202,6 +240,7 @@ describe("EventTimelineSet", () => {
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true,
addToState: false,
});
}).toThrow();
});
@@ -214,6 +253,7 @@ describe("EventTimelineSet", () => {
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
@@ -232,6 +272,7 @@ describe("EventTimelineSet", () => {
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
@@ -248,6 +289,7 @@ describe("EventTimelineSet", () => {
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
});
@@ -257,7 +299,7 @@ describe("EventTimelineSet", () => {
describe("aggregateRelations", () => {
describe("with unencrypted events", () => {
beforeEach(() => {
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
itShouldReturnTheRelatedEvents();
@@ -279,7 +321,7 @@ describe("EventTimelineSet", () => {
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
it("should not return the related events", () => {
+27 -23
View File
@@ -98,7 +98,7 @@ describe("EventTimeline", function () {
expect(function () {
timeline.initialiseState(state);
}).not.toThrow();
timeline.addEvent(event, { toStartOfTimeline: false });
timeline.addEvent(event, { toStartOfTimeline: false, addToState: false });
expect(function () {
timeline.initialiseState(state);
}).toThrow();
@@ -182,9 +182,9 @@ describe("EventTimeline", function () {
];
it("should be able to add events to the end", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]);
@@ -192,9 +192,9 @@ describe("EventTimeline", function () {
});
it("should be able to add events to the start", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]);
@@ -238,9 +238,9 @@ describe("EventTimeline", function () {
content: { name: "Old Room Name" },
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.sender).toEqual(oldSentinel);
});
@@ -280,9 +280,9 @@ describe("EventTimeline", function () {
skey: userA,
event: true,
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.target).toEqual(oldSentinel);
});
@@ -308,8 +308,8 @@ describe("EventTimeline", function () {
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: true });
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
@@ -347,8 +347,8 @@ describe("EventTimeline", function () {
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: true });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
@@ -365,11 +365,15 @@ describe("EventTimeline", function () {
);
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false })).not.toThrow();
// @ts-ignore stateContext is not a valid param
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
expect(() =>
timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }),
timeline.addEvent(events[0], {
toStartOfTimeline: false,
addToState: false,
roomState: new RoomState(roomId),
}),
).not.toThrow();
});
});
@@ -397,8 +401,8 @@ describe("EventTimeline", function () {
];
it("should remove events", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId()!);
@@ -411,9 +415,9 @@ describe("EventTimeline", function () {
});
it("should update baseIndex", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[2], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
@@ -430,11 +434,11 @@ describe("EventTimeline", function () {
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
timeline.removeEvent(events[0].getId()!);
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
+2 -2
View File
@@ -20,7 +20,7 @@ import { logger } from "../../src/logger";
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
import { HTTPError, MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { randomString } from "../../src/randomstring";
import { secureRandomString } from "../../src/randomstring";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
@@ -502,7 +502,7 @@ describe("InteractiveAuth", () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const sid = randomString(24);
const sid = secureRandomString(24);
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
const ia = new InteractiveAuth({
+104 -31
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { Mocked, mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import { logger } from "../../src/logger";
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
@@ -76,6 +77,7 @@ import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from ".
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { KnownMembership } from "../../src/@types/membership";
import { RoomMessageEventContent } from "../../src/@types/events";
import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
jest.useFakeTimers();
@@ -94,6 +96,12 @@ function convertQueryDictToMap(queryDict?: QueryDict): Map<string, string> {
return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)]));
}
declare module "../../src/@types/event" {
interface AccountDataEvents {
"im.vector.test": {};
}
}
type HttpLookup = {
method: string;
path: string;
@@ -259,13 +267,17 @@ describe("MatrixClient", function () {
if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: (<MatrixError>next.error).errcode,
httpStatus: (<MatrixError>next.error).httpStatus,
name: (<MatrixError>next.error).errcode,
message: "Expected testing error",
data: next.error,
});
return Promise.reject(
new MatrixError(
{
errcode: (<MatrixError>next.error).errcode,
name: (<MatrixError>next.error).errcode,
message: "Expected testing error",
data: next.error,
},
(<MatrixError>next.error).httpStatus,
),
);
}
return Promise.resolve(next.data);
}
@@ -2799,24 +2811,28 @@ describe("MatrixClient", function () {
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId),
],
{},
{ addToState: true },
);
room2.addLiveEvents(
[
roomCreateEvent(room2.roomId, replacedByCreate2.roomId),
predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId),
],
{},
{ addToState: true },
);
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {});
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {});
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {
addToState: true,
});
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {
addToState: true,
});
replacedByDynamicPredecessor1.addLiveEvents(
[tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)],
{},
{ addToState: true },
);
replacedByDynamicPredecessor2.addLiveEvents(
[tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)],
{},
{ addToState: true },
);
return {
@@ -2854,10 +2870,10 @@ describe("MatrixClient", function () {
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
client.store = new StubStore();
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {});
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {});
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {});
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {});
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
// When we ask for the visible rooms
const rooms = client.getVisibleRooms();
@@ -2937,15 +2953,15 @@ describe("MatrixClient", function () {
const room4 = new Room("room4", client, "@michonne:hawthorne.example.com");
if (creates) {
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true });
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)], { addToState: true });
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)], { addToState: true });
}
if (tombstones) {
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {});
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {});
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], { addToState: true });
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], { addToState: true });
}
mocked(store.getRoom).mockImplementation((roomId: string) => {
@@ -2980,17 +2996,17 @@ describe("MatrixClient", function () {
const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com");
const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com");
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {});
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]);
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {});
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]);
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {});
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]);
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {});
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]);
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
mocked(store.getRoom)
.mockClear()
@@ -3479,6 +3495,63 @@ describe("MatrixClient", function () {
});
});
describe("getAuthMetadata", () => {
beforeEach(() => {
fetchMock.mockReset();
// This request is made by oidc-client-ts so is not intercepted by httpLookups
fetchMock.get("https://auth.org/jwks", {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
});
it("should use unstable prefix", async () => {
const metadata = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
data: metadata,
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: [],
});
expect(httpLookups.length).toEqual(0);
});
it("should fall back to auth_issuer + openid-configuration", async () => {
const metadata = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
{
method: "GET",
path: `/auth_issuer`,
data: { issuer: metadata.issuer },
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: [],
});
expect(httpLookups.length).toEqual(0);
});
});
describe("identityHashedLookup", () => {
it("should return hashed lookup results", async () => {
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
+19 -112
View File
@@ -15,7 +15,8 @@ limitations under the License.
*/
import { MatrixEvent } from "../../../src";
import { CallMembership, CallMembershipDataLegacy, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { CallMembership, SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership";
import { membershipTemplate } from "./mocks";
function makeMockEvent(originTs = 0): MatrixEvent {
return {
@@ -25,91 +26,15 @@ function makeMockEvent(originTs = 0): MatrixEvent {
}
describe("CallMembership", () => {
describe("CallMembershipDataLegacy", () => {
const membershipTemplate: CallMembershipDataLegacy = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
membershipID: "bloop",
foci_active: [{ type: "livekit" }],
};
it("rejects membership with no expiry and no expires_ts", () => {
expect(() => {
new CallMembership(
makeMockEvent(),
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
);
}).toThrow();
});
it("rejects membership with no device_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
}).toThrow();
});
it("rejects membership with no call_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
}).toThrow();
});
it("allow membership with no scope", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).not.toThrow();
});
it("rejects with malformatted expires_ts", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" }));
}).toThrow();
});
it("rejects with malformatted expires", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" }));
}).toThrow();
});
it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
expect(membership.createdTs()).toEqual(12345);
});
it("uses created_ts if present", () => {
const membership = new CallMembership(
makeMockEvent(12345),
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
);
expect(membership.createdTs()).toEqual(67890);
});
it("computes absolute expiry time based on expires", () => {
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
it("computes absolute expiry time based on expires_ts", () => {
const membership = new CallMembership(
makeMockEvent(1000),
Object.assign({}, membershipTemplate, { expires_ts: 6000 }),
);
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(
fakeEvent,
Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }),
);
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
});
});
describe("SessionMembershipData", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
const membershipTemplate: SessionMembershipData = {
call_id: "",
scope: "m.room",
@@ -152,9 +77,14 @@ describe("CallMembership", () => {
it("considers memberships unexpired if local age low enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(false);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
});
it("considers memberships expired if local age large enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
});
it("returns preferred foci", () => {
@@ -171,15 +101,6 @@ describe("CallMembership", () => {
describe("expiry calculation", () => {
let fakeEvent: MatrixEvent;
let membership: CallMembership;
const membershipTemplate: CallMembershipDataLegacy = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
membershipID: "bloop",
foci_active: [{ type: "livekit" }],
};
beforeEach(() => {
// server origin timestamp for this event is 1000
@@ -193,24 +114,10 @@ describe("CallMembership", () => {
jest.useRealTimers();
});
it("converts expiry time into local clock", () => {
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
// (ie. the local clock is 1 second ahead of the servers' clocks)
fakeEvent.localTimestamp = 2000;
// for simplicity's sake, we say that the event's age is zero
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
// for sanity's sake, make sure the server-relative expiry time is what we expect
expect(membership.getAbsoluteExpiry()).toEqual(6000);
// therefore the expiry time converted to our clock should be 1 second later
expect(membership.getLocalExpiry()).toEqual(7000);
});
it("calculates time until expiry", () => {
jest.setSystemTime(2000);
// should be using absolute expiry time
expect(membership.getMsUntilExpiry()).toEqual(4000);
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
});
});
});
+182 -341
View File
@@ -14,30 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { encodeBase64, EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { KnownMembership } from "../../../src/@types/membership";
import {
CallMembershipData,
CallMembershipDataLegacy,
SessionMembershipData,
} from "../../../src/matrixrtc/CallMembership";
import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { randomString } from "../../../src/randomstring";
import { makeMockRoom, makeMockRoomState, mockRTCEvent } from "./mocks";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }],
};
import { secureRandomString } from "../../../src/randomstring";
import { flushPromises } from "../../test-utils/flushPromises";
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
const mockFocus = { type: "mock" };
const textEncoder = new TextEncoder();
describe("MatrixRTCSession", () => {
let client: MatrixClient;
let sess: MatrixRTCSession | undefined;
@@ -57,7 +46,7 @@ describe("MatrixRTCSession", () => {
describe("roomSessionForRoom", () => {
it("creates a room-scoped session from room state", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(1);
@@ -65,7 +54,6 @@ describe("MatrixRTCSession", () => {
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].membershipID).toEqual("bloop");
expect(sess?.memberships[0].isExpired()).toEqual(false);
expect(sess?.callId).toEqual("");
});
@@ -85,7 +73,7 @@ describe("MatrixRTCSession", () => {
});
it("ignores memberships events of members not in the room", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(0);
@@ -110,7 +98,7 @@ describe("MatrixRTCSession", () => {
});
it("safely ignores events with no memberships section", () => {
const roomId = randomString(8);
const roomId = secureRandomString(8);
const event = {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({}),
@@ -145,7 +133,7 @@ describe("MatrixRTCSession", () => {
});
it("safely ignores events with junk memberships section", () => {
const roomId = randomString(8);
const roomId = secureRandomString(8);
const event = {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
@@ -179,14 +167,6 @@ describe("MatrixRTCSession", () => {
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no expires_ts", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
(expiredMembership.expires as number | undefined) = undefined;
const mockRoom = makeMockRoom([expiredMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no device_id", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.device_id as string | undefined) = undefined;
@@ -222,23 +202,7 @@ describe("MatrixRTCSession", () => {
describe("updateCallMembershipEvent", () => {
const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" };
const joinSessionConfig = { useLegacyMemberEvents: false };
const legacyMembershipData: CallMembershipDataLegacy = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA_legacy",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [mockFocus],
};
const expiredLegacyMembershipData: CallMembershipDataLegacy = {
...legacyMembershipData,
device_id: "AAAAAAA_legacy_expired",
expires: 0,
};
const joinSessionConfig = {};
const sessionMembershipData: SessionMembershipData = {
call_id: "",
@@ -271,39 +235,20 @@ describe("MatrixRTCSession", () => {
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
});
async function testSession(
membershipData: CallMembershipData[] | SessionMembershipData,
shouldUseLegacy: boolean,
): Promise<void> {
async function testSession(membershipData: SessionMembershipData): Promise<void> {
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
const makeNewLegacyMembershipsMock = jest.spyOn(sess as any, "makeNewLegacyMemberships");
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(makeNewLegacyMembershipsMock).toHaveBeenCalledTimes(shouldUseLegacy ? 1 : 0);
expect(makeNewMembershipMock).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
expect(sendStateEventMock).toHaveBeenCalledTimes(1);
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
expect(sendDelayedStateMock).toHaveBeenCalledTimes(1);
}
it("uses legacy events if there are any active legacy calls", async () => {
await testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
});
it('uses legacy events if a non-legacy call is in a "memberships" array', async () => {
await testSession([sessionMembershipData], true);
});
it("uses non-legacy events if all legacy calls are expired", async () => {
await testSession([expiredLegacyMembershipData], false);
});
it("uses non-legacy events if there are only non-legacy calls", async () => {
await testSession(sessionMembershipData, false);
it("sends events", async () => {
await testSession(sessionMembershipData);
});
});
@@ -324,7 +269,11 @@ describe("MatrixRTCSession", () => {
});
describe("getsActiveFocus", () => {
const activeFociConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const firstPreferredFocus = {
type: "livekit",
livekit_service_url: "https://active.url",
livekit_alias: "!active:active.url",
};
it("gets the correct active focus with oldest_membership", () => {
jest.useFakeTimers();
jest.setSystemTime(3000);
@@ -332,7 +281,7 @@ describe("MatrixRTCSession", () => {
Object.assign({}, membershipTemplate, {
device_id: "foo",
created_ts: 500,
foci_active: [activeFociConfig],
foci_preferred: [firstPreferredFocus],
}),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
@@ -344,15 +293,15 @@ describe("MatrixRTCSession", () => {
type: "livekit",
focus_selection: "oldest_membership",
});
expect(sess.getActiveFocus()).toBe(activeFociConfig);
expect(sess.getActiveFocus()).toBe(firstPreferredFocus);
jest.useRealTimers();
});
it("does not provide focus if the selction method is unknown", () => {
it("does not provide focus if the selection method is unknown", () => {
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, {
device_id: "foo",
created_ts: 500,
foci_active: [activeFociConfig],
foci_preferred: [firstPreferredFocus],
}),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
@@ -366,25 +315,6 @@ describe("MatrixRTCSession", () => {
});
expect(sess.getActiveFocus()).toBe(undefined);
});
it("gets the correct active focus legacy", () => {
jest.useFakeTimers();
jest.setSystemTime(3000);
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, {
device_id: "foo",
created_ts: 500,
foci_active: [activeFociConfig],
}),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }]);
expect(sess.getActiveFocus()).toBe(activeFociConfig);
jest.useRealTimers();
});
});
describe("joining", () => {
@@ -392,9 +322,11 @@ describe("MatrixRTCSession", () => {
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sendEventMock: jest.Mock;
let updateDelayedEventMock: jest.Mock;
let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
let updatedDelayedEvent: Promise<void>;
beforeEach(() => {
sentStateEvent = new Promise((resolve) => {
@@ -408,12 +340,15 @@ describe("MatrixRTCSession", () => {
};
});
});
updatedDelayedEvent = new Promise((r) => {
updateDelayedEventMock = jest.fn(r);
});
sendEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
client.sendEvent = sendEventMock;
client._unstable_updateDelayedEvent = jest.fn();
client._unstable_updateDelayedEvent = updateDelayedEventMock;
mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
@@ -446,24 +381,28 @@ describe("MatrixRTCSession", () => {
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000,
expires_ts: Date.now() + 3600000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: DEFAULT_EXPIRE_DURATION,
foci_preferred: [mockFocus],
focus_active: {
focus_selection: "oldest_membership",
type: "livekit",
},
},
"@alice:example.org",
"_@alice:example.org_AAAAAAA",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
// Because we actually want to send the state
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
// For checking if the delayed event is still there or got removed while sending the state.
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
// For scheduling the delayed event
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
// This returns no error so we do not check if we reschedule the event again. this is done in another test.
jest.useRealTimers();
});
@@ -476,28 +415,25 @@ describe("MatrixRTCSession", () => {
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 60000,
expires_ts: Date.now() + 60000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 60000,
foci_preferred: [mockFocus],
focus_active: {
focus_selection: "oldest_membership",
type: "livekit",
},
},
"@alice:example.org",
"_@alice:example.org_AAAAAAA",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
describe("non-legacy calls", () => {
describe("calls", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
@@ -521,6 +457,7 @@ describe("MatrixRTCSession", () => {
});
});
const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`;
// preparing the delayed disconnect should handle ratelimiting
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
@@ -545,25 +482,18 @@ describe("MatrixRTCSession", () => {
});
});
// needed to advance the mock timers properly
const scheduledDelayDisconnection = new Promise<void>((resolve) => {
const originalFn: () => void = (sess as any).scheduleDelayDisconnection;
(sess as any).scheduleDelayDisconnection = jest.fn(() => {
originalFn.call(sess);
resolve();
});
});
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
useLegacyMemberEvents: false,
membershipServerSideExpiryTimeout: 9000,
});
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 9000);
await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 7500);
await sendDelayedStateAttempt;
const callProps = (d: number) => {
return [mockRoom!.roomId, { delay: d }, "org.matrix.msc3401.call.member", {}, userStateKey];
};
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(1, ...callProps(9000));
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(2, ...callProps(7500));
jest.advanceTimersByTime(5000);
await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
@@ -577,26 +507,29 @@ describe("MatrixRTCSession", () => {
application: "m.call",
scope: "m.room",
call_id: "",
expires: 14400000,
device_id: "AAAAAAA",
foci_preferred: [activeFocusConfig],
focus_active: activeFocus,
} satisfies SessionMembershipData,
`${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`,
userStateKey,
);
await sentDelayedState;
// should have prepared the heartbeat to keep delaying the leave event while still connected
await scheduledDelayDisconnection;
// should have tried updating the delayed leave to test that it wasn't replaced by own state
await updatedDelayedEvent;
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
// should update delayed disconnect
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
await flushPromises();
jest.advanceTimersByTime(5000);
// should update delayed disconnect
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
jest.useRealTimers();
}
it("sends a membership event with session payload when joining a non-legacy call", async () => {
it("sends a membership event with session payload when joining a call", async () => {
await testJoin(false);
});
@@ -605,133 +538,63 @@ describe("MatrixRTCSession", () => {
});
});
it("does nothing if join called when already joined", () => {
it("does nothing if join called when already joined", async () => {
sess!.joinRoomSession([mockFocus], mockFocus);
await sentStateEvent;
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
sess!.joinRoomSession([mockFocus], mockFocus);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
});
it("renews membership event before expiry time", async () => {
jest.useFakeTimers();
let resolveFn: ((_roomId: string, _type: string, val: Record<string, any>) => void) | undefined;
const eventSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
r(val);
};
});
try {
const sendStateEventMock = jest.fn().mockImplementation(resolveFn);
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus], mockFocus);
const eventContent = await eventSentPromise;
jest.setSystemTime(1000);
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId);
const getState = mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
getState.getStateEvents = jest.fn().mockReturnValue(event);
getState.events = new Map([
[
event.getType(),
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
} as unknown as Map<string, MatrixEvent>,
],
]);
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
r(val);
};
});
sendStateEventMock.mockReset().mockImplementation(resolveFn);
// definitely should have renewed by 1 second before the expiry!
const timeElapsed = 60 * 60 * 1000 - 1000;
jest.setSystemTime(Date.now() + timeElapsed);
jest.advanceTimersByTime(timeElapsed);
await eventReSentPromise;
expect(sendStateEventMock).toHaveBeenCalledWith(
mockRoom.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000 * 2,
expires_ts: 1000 + 3600000 * 2,
foci_active: [mockFocus],
created_ts: 1000,
membershipID: expect.stringMatching(".*"),
},
],
},
"@alice:example.org",
);
} finally {
jest.useRealTimers();
}
});
});
describe("onMembershipsChanged", () => {
it("does not emit if no membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
expect(onMembershipsChanged).not.toHaveBeenCalled();
});
it("emits on membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
mockRoom.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState([], mockRoom.roomId));
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
expect(onMembershipsChanged).toHaveBeenCalled();
});
it("emits an event at the time a membership event expires", () => {
jest.useFakeTimers();
try {
const membership = Object.assign({}, membershipTemplate);
const mockRoom = makeMockRoom([membership]);
// TODO: re-enable this test when expiry is implemented
// eslint-disable-next-line jest/no-commented-out-tests
// it("emits an event at the time a membership event expires", () => {
// jest.useFakeTimers();
// try {
// const membership = Object.assign({}, membershipTemplate);
// const mockRoom = makeMockRoom([membership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const membershipObject = sess.memberships[0];
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
// const membershipObject = sess.memberships[0];
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
// const onMembershipsChanged = jest.fn();
// sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
jest.advanceTimersByTime(61 * 1000 * 1000);
// jest.advanceTimersByTime(61 * 1000 * 1000);
expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
expect(sess?.memberships.length).toEqual(0);
} finally {
jest.useRealTimers();
}
});
// expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
// expect(sess?.memberships.length).toEqual(0);
// } finally {
// jest.useRealTimers();
// }
// });
});
describe("key management", () => {
@@ -803,9 +666,13 @@ describe("MatrixRTCSession", () => {
}
});
it("does not send key if join called when already joined", () => {
it("does not send key if join called when already joined", async () => {
const sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await sentStateEvent;
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(client.sendEvent).toHaveBeenCalledTimes(1);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
@@ -864,7 +731,64 @@ describe("MatrixRTCSession", () => {
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
});
it("Re-sends key if a new member joins", async () => {
it("re-sends key if a new member joins even if a key rotation is in progress", async () => {
jest.useFakeTimers();
try {
// session with two members
const member2 = Object.assign({}, membershipTemplate, {
device_id: "BBBBBBB",
});
const mockRoom = makeMockRoom([membershipTemplate, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
// joining will trigger an initial key send
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
sess.joinRoomSession([mockFocus], mockFocus, {
manageMediaKeys: true,
updateEncryptionKeyThrottle: 1000,
makeKeyDelay: 3000,
});
await keysSentPromise1;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
// member2 leaves triggering key rotation
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
sess.onRTCSessionMemberUpdate();
// member2 re-joins which should trigger an immediate re-send
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onRTCSessionMemberUpdate();
// but, that immediate resend is throttled so we need to wait a bit
jest.advanceTimersByTime(1000);
const { keys } = await keysSentPromise2;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
// key index should still be the original: 0
expect(keys[0].index).toEqual(0);
// check that the key rotation actually happens
const keysSentPromise3 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
jest.advanceTimersByTime(2000);
const { keys: rotatedKeys } = await keysSentPromise3;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(3);
// key index should now be the rotated one: 1
expect(rotatedKeys[0].index).toEqual(1);
} finally {
jest.useRealTimers();
}
});
it("re-sends key if a new member joins", async () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
@@ -895,7 +819,7 @@ describe("MatrixRTCSession", () => {
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
await keysSentPromise2;
@@ -949,7 +873,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockClear();
// these should be a no-op:
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
expect(sendEventMock).toHaveBeenCalledTimes(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
} finally {
@@ -957,89 +881,6 @@ describe("MatrixRTCSession", () => {
}
});
it("re-sends key if a member changes membership ID", async () => {
jest.useFakeTimers();
try {
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
const member1 = membershipTemplate;
const member2 = {
...membershipTemplate,
device_id: "BBBBBBB",
};
const mockRoom = makeMockRoom([member1, member2]);
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
// make sure an encryption key was sent
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
sendEventMock.mockClear();
// this should be a no-op:
sess.onMembershipUpdate();
expect(sendEventMock).toHaveBeenCalledTimes(0);
// advance time to avoid key throttling
jest.advanceTimersByTime(10000);
// update membership ID
member2.membershipID = "newID";
const keysSentPromise2 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
// this should re-send the key
sess.onMembershipUpdate();
await keysSentPromise2;
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
}
});
it("re-sends key if a member changes created_ts", async () => {
jest.useFakeTimers();
jest.setSystemTime(1000);
@@ -1086,7 +927,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockClear();
// this should be a no-op:
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
expect(sendEventMock).toHaveBeenCalledTimes(0);
// advance time to avoid key throttling
@@ -1100,7 +941,7 @@ describe("MatrixRTCSession", () => {
});
// this should re-send the key
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
await keysSentPromise2;
@@ -1163,7 +1004,7 @@ describe("MatrixRTCSession", () => {
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
jest.advanceTimersByTime(10000);
@@ -1181,7 +1022,7 @@ describe("MatrixRTCSession", () => {
it("wraps key index around to 0 when it reaches the maximum", async () => {
// this should give us keys with index [0...255, 0, 1]
const membersToTest = 258;
const members: CallMembershipData[] = [];
const members: SessionMembershipData[] = [];
for (let i = 0; i < membersToTest; i++) {
members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` }));
}
@@ -1208,7 +1049,7 @@ describe("MatrixRTCSession", () => {
);
}
sess!.onMembershipUpdate();
sess!.onRTCSessionMemberUpdate();
// advance time to avoid key throttling
jest.advanceTimersByTime(10000);
@@ -1249,7 +1090,7 @@ describe("MatrixRTCSession", () => {
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
sess.onRTCSessionMemberUpdate();
await new Promise((resolve) => {
realSetTimeout(resolve);
@@ -1288,7 +1129,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1320,7 +1161,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
4,
"@bob:example.org:bobsphone",
);
@@ -1352,7 +1193,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1379,12 +1220,12 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
4,
"@bob:example.org:bobsphone",
);
@@ -1432,7 +1273,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("newer key", "utf-8"),
textEncoder.encode("newer key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1480,7 +1321,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("second key", "utf-8"),
textEncoder.encode("second key"),
0,
"@bob:example.org:bobsphone",
);
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Mock } from "jest-mock";
import {
ClientEvent,
EventTimeline,
@@ -24,19 +26,8 @@ import {
RoomEvent,
} from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom } from "./mocks";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [{ type: "test" }],
};
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;
@@ -69,16 +60,15 @@ describe("MatrixRTCSessionManager", () => {
it("Fires event when session ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const memberships = [membershipTemplate];
const room1 = makeMockRoom(memberships);
const room1 = makeMockRoom(membershipTemplate);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
memberships.splice(0, 1);
(room1.getLiveTimeline as Mock).mockReturnValue({
getState: jest.fn().mockReturnValue(makeMockRoomState([{}], room1.roomId)),
});
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("")[0];
+51 -26
View File
@@ -15,16 +15,36 @@ limitations under the License.
*/
import { EventType, MatrixEvent, Room } from "../../../src";
import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { randomString } from "../../../src/randomstring";
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { secureRandomString } from "../../../src/randomstring";
type MembershipData = CallMembershipData[] | SessionMembershipData;
type MembershipData = SessionMembershipData[] | SessionMembershipData | {};
export const membershipTemplate: SessionMembershipData = {
application: "m.call",
call_id: "",
device_id: "AAAAAAA",
scope: "m.room",
focus_active: { type: "livekit", livekit_service_url: "https://lk.url" },
foci_preferred: [
{
livekit_alias: "!alias:something.org",
livekit_service_url: "https://livekit-jwt.something.io",
type: "livekit",
},
{
livekit_alias: "!alias:something.org",
livekit_service_url: "https://livekit-jwt.something.dev",
type: "livekit",
},
],
};
export function makeMockRoom(membershipData: MembershipData): Room {
const roomId = randomString(8);
const roomId = secureRandomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(membershipData, roomId);
return {
const room = {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
getLiveTimeline: jest.fn().mockReturnValue({
@@ -32,41 +52,46 @@ export function makeMockRoom(membershipData: MembershipData): Room {
}),
getVersion: jest.fn().mockReturnValue("default"),
} as unknown as Room;
return room;
}
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
const event = mockRTCEvent(membershipData, roomId);
const events = Array.isArray(membershipData)
? membershipData.map((m) => mockRTCEvent(m, roomId))
: [mockRTCEvent(membershipData, roomId)];
const keysAndEvents = events.map((e) => {
const data = e.getContent() as SessionMembershipData;
return [`_${e.sender?.userId}_${data.device_id}`];
});
return {
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_: string, stateKey: string) => {
if (stateKey !== undefined) return event;
return [event];
if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1];
return events;
},
events: new Map([
[
event.getType(),
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
events:
events.length === 0
? new Map()
: new Map([
[
EventType.GroupCallMemberPrefix,
{
size: () => true,
has: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey),
get: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey)?.[1],
values: () => events,
},
],
]),
};
}
export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue(
!Array.isArray(membershipData)
? membershipData
: {
memberships: membershipData,
},
),
getContent: jest.fn().mockReturnValue(membershipData),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(Date.now()),
getRoomId: jest.fn().mockReturnValue(roomId),
+6 -6
View File
@@ -99,7 +99,7 @@ describe("MatrixEvent", () => {
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const ev = createEvent("$event1:server");
await room.addLiveEvents([ev]);
await room.addLiveEvents([ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
@@ -120,7 +120,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -143,7 +143,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -167,7 +167,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const reaction = createReactionEvent("$reaction:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, reaction]);
await room.addLiveEvents([threadRoot, ev, reaction], { addToState: false });
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -191,7 +191,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const edit = createEditEvent("$edit:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, edit]);
await room.addLiveEvents([threadRoot, ev, edit], { addToState: false });
await room.createThreadsTimelineSets();
expect(edit.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -217,7 +217,7 @@ describe("MatrixEvent", () => {
const reply2 = createReplyEvent("$reply2:server", reply1.getId()!);
const reaction = createReactionEvent("$reaction:server", reply2.getId()!);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction], { addToState: false });
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
+22 -22
View File
@@ -36,7 +36,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// When I ask about any event, then it is unread
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false);
@@ -46,7 +46,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEventSentBy(readerId);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// When I ask about an event I sent, it is read (because a synthetic
// receipt was created and stored in RoomReceipts)
@@ -57,7 +57,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
@@ -72,7 +72,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [event1, event1Id] = createEvent();
const [event2] = createEvent();
room.addLiveEvents([event1, event2]);
room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createReceipt(readerId, event2));
@@ -86,7 +86,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [oldEvent, oldEventId] = createEvent();
const [liveEvent] = createEvent();
room.addLiveEvents([liveEvent]);
room.addLiveEvents([liveEvent], { addToState: false });
createOldTimeline(room, [oldEvent]);
// When we receive a receipt for the live event
@@ -120,7 +120,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
room.addLiveEvents([event1, event2]);
room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the earlier event
room.addReceipt(createReceipt(readerId, event1));
@@ -133,7 +133,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
@@ -151,7 +151,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [previousEvent] = createEvent();
const [myEvent] = createEventSentBy(readerId);
room.addLiveEvents([previousEvent, myEvent]);
room.addLiveEvents([previousEvent, myEvent], { addToState: false });
// And I just received a receipt for the previous event
room.addReceipt(createReceipt(readerId, previousEvent));
@@ -165,7 +165,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [myEvent] = createEventSentBy(readerId);
const [laterEvent] = createEvent();
room.addLiveEvents([myEvent, laterEvent]);
room.addLiveEvents([myEvent, laterEvent], { addToState: false });
// When I ask about the later event, it is unread (because it's after the synthetic receipt)
expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false);
@@ -177,7 +177,7 @@ describe("RoomReceipts", () => {
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
const [event3, event3Id] = createEvent();
room.addLiveEvents([event1, event2, event3]);
room.addLiveEvents([event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order
room.addReceipt(createReceipt(readerId, event2));
@@ -192,7 +192,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
@@ -208,7 +208,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event on this thread
@@ -225,7 +225,7 @@ describe("RoomReceipts", () => {
const [event1, event1Id] = createThreadedEvent(root);
const [event2] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@@ -241,7 +241,7 @@ describe("RoomReceipts", () => {
const [event1] = createThreadedEvent(root);
const [event2, event2Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the earlier event
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
@@ -256,7 +256,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
@@ -278,7 +278,7 @@ describe("RoomReceipts", () => {
const [thread2] = createThreadedEvent(root2);
setupThread(room, root1);
setupThread(room, root2);
room.addLiveEvents([root1, root2, thread1, thread2]);
room.addLiveEvents([root1, root2, thread1, thread2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!));
@@ -295,7 +295,7 @@ describe("RoomReceipts", () => {
const [event2, event2Id] = createThreadedEvent(root);
const [event3, event3Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2, event3]);
room.addLiveEvents([root, event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@@ -329,7 +329,7 @@ describe("RoomReceipts", () => {
const [thread2b, thread2bId] = createThreadedEvent(main2);
setupThread(room, main1);
setupThread(room, main2);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b]);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b], { addToState: false });
// And the timestamps on the events are consistent with the order above
main1.event.origin_server_ts = 1;
@@ -377,7 +377,7 @@ describe("RoomReceipts", () => {
// Add the event to the room
// The receipt is removed from the dangling state
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@@ -398,7 +398,7 @@ describe("RoomReceipts", () => {
// Add the events to the room
// The receipt is removed from the dangling state
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@@ -418,7 +418,7 @@ describe("RoomReceipts", () => {
// Add the event to the room
// The two receipts should be processed
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// Then the event is read
// We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId`
@@ -528,7 +528,7 @@ function createThreadedReceipt(userId: string, referencedEvent: MatrixEvent, thr
*/
function createOldTimeline(room: Room, events: MatrixEvent[]) {
const oldTimeline = room.getUnfilteredTimelineSet().addTimeline();
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, oldTimeline);
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, false, oldTimeline);
}
/**
+1 -1
View File
@@ -801,7 +801,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
// Ensure the root is in the room timeline
root.setThreadId(root.getId());
await room.addLiveEvents([root]);
await room.addLiveEvents([root], { addToState: false });
// Create the thread and wait for it to be initialised
const thread = room.createThread(root.getId()!, root, [], false);
+1 -1
View File
@@ -106,7 +106,7 @@ describe("fixNotificationCountOnDecryption", () => {
mockClient,
);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
THREAD_ID = event.getId()!;
threadEvent = mkEvent({
+5 -12
View File
@@ -40,8 +40,8 @@ jest.mock("jwt-decode");
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
const authorizationEndpoint = delegatedAuthConfig.authorization_endpoint;
const tokenEndpoint = delegatedAuthConfig.token_endpoint;
const clientId = "xyz789";
const baseUrl = "https://test.com";
@@ -52,10 +52,7 @@ describe("oidc authorization", () => {
jest.spyOn(logger, "warn");
jest.setSystemTime(now);
fetchMock.get(
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
globalThis.TextEncoder = TextEncoder;
});
@@ -127,11 +124,9 @@ describe("oidc authorization", () => {
it("should generate url with correct parameters", async () => {
const nonce = "abc123";
const metadata = delegatedAuthConfig.metadata;
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
@@ -156,11 +151,9 @@ describe("oidc authorization", () => {
it("should generate url with create prompt", async () => {
const nonce = "abc123";
const metadata = delegatedAuthConfig.metadata;
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
+6 -9
View File
@@ -42,13 +42,13 @@ describe("registerOidcClient()", () => {
});
it("should make correct request to register client", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registrationEndpoint!,
delegatedAuthConfig.registration_endpoint,
expect.objectContaining({
headers: {
"Accept": "application/json",
@@ -72,7 +72,7 @@ describe("registerOidcClient()", () => {
});
it("should throw when registration request fails", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 500,
});
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
@@ -81,7 +81,7 @@ describe("registerOidcClient()", () => {
});
it("should throw when registration response is invalid", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200,
// no clientId in response
body: "{}",
@@ -96,7 +96,7 @@ describe("registerOidcClient()", () => {
registerOidcClient(
{
...delegatedAuthConfig,
registrationEndpoint: undefined,
registration_endpoint: undefined,
},
metadata,
),
@@ -108,10 +108,7 @@ describe("registerOidcClient()", () => {
registerOidcClient(
{
...delegatedAuthConfig,
metadata: {
...delegatedAuthConfig.metadata,
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
},
grant_types_supported: [delegatedAuthConfig.grant_types_supported[0]],
},
metadata,
),
+11 -11
View File
@@ -55,8 +55,8 @@ describe("OidcTokenRefresher", () => {
});
beforeEach(() => {
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config);
fetchMock.get(`${config.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
keys: [],
});
fetchMock.post(config.tokenEndpoint, {
fetchMock.post(config.token_endpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -81,7 +81,7 @@ describe("OidcTokenRefresher", () => {
it("throws when oidc client cannot be initialised", async () => {
jest.spyOn(logger, "error");
fetchMock.get(
`${config.metadata.issuer}.well-known/openid-configuration`,
`${config.issuer}.well-known/openid-configuration`,
{
ok: false,
status: 404,
@@ -126,7 +126,7 @@ describe("OidcTokenRefresher", () => {
const result = await refresher.doRefreshAccessToken("refresh-token");
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
expect(fetchMock).toHaveFetched(config.token_endpoint, {
method: "POST",
});
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
it("should only have one inflight refresh request at once", async () => {
fetchMock
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
const result2 = await first;
// only one call to token endpoint
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
expect(fetchMock).toHaveFetchedTimes(1, config.token_endpoint);
expect(result1).toEqual({
accessToken: "first-new-access-token",
refreshToken: "first-new-refresh-token",
@@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
it("should log and rethrow when token refresh fails", async () => {
fetchMock.post(
config.tokenEndpoint,
config.token_endpoint,
{
status: 503,
headers: {
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
// make sure inflight request is cleared after a failure
fetchMock
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 503,
headers: {
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
+48 -23
View File
@@ -18,17 +18,18 @@ import { mocked } from "jest-mock";
import { jwtDecode } from "jwt-decode";
import { logger } from "../../../src/logger";
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
import { ValidatedAuthMetadata, validateIdToken, validateAuthMetadata } from "../../../src/oidc/validate";
import { OidcError } from "../../../src/oidc/error";
jest.mock("jwt-decode");
describe("validateOIDCIssuerWellKnown", () => {
const validWk: any = {
const validWk: ValidatedAuthMetadata = {
issuer: "https://test.org",
authorization_endpoint: "https://test.org/authorize",
token_endpoint: "https://authorize.org/token",
registration_endpoint: "https://authorize.org/regsiter",
revocation_endpoint: "https://authorize.org/regsiter",
registration_endpoint: "https://authorize.org/register",
revocation_endpoint: "https://authorize.org/revoke",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
@@ -44,14 +45,14 @@ describe("validateOIDCIssuerWellKnown", () => {
it("should throw OP support error when wellKnown is not an object", () => {
expect(() => {
validateOIDCIssuerWellKnown([]);
validateAuthMetadata([]);
}).toThrow(OidcError.OpSupport);
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
});
it("should log all errors before throwing", () => {
expect(() => {
validateOIDCIssuerWellKnown({
validateAuthMetadata({
...validWk,
authorization_endpoint: undefined,
response_types_supported: [],
@@ -62,24 +63,31 @@ describe("validateOIDCIssuerWellKnown", () => {
});
it("should return validated issuer config", () => {
expect(validateOIDCIssuerWellKnown(validWk)).toEqual({
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: validWk.registration_endpoint,
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
accountManagementEndpoint: "https://authorize.org/account",
});
expect(validateAuthMetadata(validWk)).toEqual(
expect.objectContaining({
issuer: validWk.issuer,
authorization_endpoint: validWk.authorization_endpoint,
token_endpoint: validWk.token_endpoint,
registration_endpoint: validWk.registration_endpoint,
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
account_management_uri: "https://authorize.org/account",
}),
);
});
it("should return validated issuer config without registrationendpoint", () => {
const wk = { ...validWk };
delete wk.registration_endpoint;
expect(validateOIDCIssuerWellKnown(wk)).toEqual({
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: undefined,
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
accountManagementEndpoint: "https://authorize.org/account",
it("should return validated issuer config without registration_endpoint", () => {
const { registration_endpoint: _, ...wk } = validWk;
expect(validateAuthMetadata(wk)).toEqual({
issuer: validWk.issuer,
authorization_endpoint: validWk.authorization_endpoint,
token_endpoint: validWk.token_endpoint,
revocation_endpoint: validWk.revocation_endpoint,
registration_endpoint: undefined,
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
account_management_uri: "https://authorize.org/account",
code_challenge_methods_supported: ["S256"],
grant_types_supported: ["authorization_code"],
response_types_supported: ["code"],
});
});
@@ -106,7 +114,7 @@ describe("validateOIDCIssuerWellKnown", () => {
...validWk,
[key]: value,
};
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport);
expect(() => validateAuthMetadata(wk)).toThrow(OidcError.OpSupport);
});
});
@@ -170,6 +178,23 @@ describe("validateIdToken()", () => {
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
});
it("should not throw when audience is an array that includes clientId", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
aud: [clientId],
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow();
});
it("should throw when audience is an array that does not include clientId", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
aud: [`${clientId},uiop`, "asdf"],
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
});
it("should throw when nonce does not match", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
+66 -18
View File
@@ -16,13 +16,18 @@ limitations under the License.
import { decodeBase64 } from "../../src/base64";
import {
randomLowercaseString,
randomString,
randomUppercaseString,
secureRandomString,
secureRandomBase64Url,
secureRandomStringFrom,
LOWERCASE,
UPPERCASE,
} from "../../src/randomstring";
describe("Random strings", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it.each([8, 16, 32])("secureRandomBase64 generates %i valid base64 bytes", (n: number) => {
const randb641 = secureRandomBase64Url(n);
const randb642 = secureRandomBase64Url(n);
@@ -33,34 +38,77 @@ describe("Random strings", () => {
expect(decoded).toHaveLength(n);
});
it.each([8, 16, 32])("randomString generates string of %i characters", (n: number) => {
const rand1 = randomString(n);
const rand2 = randomString(n);
it.each([8, 16, 32])("secureRandomString generates string of %i characters", (n: number) => {
const rand1 = secureRandomString(n);
const rand2 = secureRandomString(n);
expect(rand1).not.toEqual(rand2);
expect(rand1).toHaveLength(n);
});
it.each([8, 16, 32])("randomLowercaseString generates lowercase string of %i characters", (n: number) => {
const rand1 = randomLowercaseString(n);
const rand2 = randomLowercaseString(n);
it.each([8, 16, 32])(
"secureRandomStringFrom generates lowercase string of %i characters when given lowercase",
(n: number) => {
const rand1 = secureRandomStringFrom(n, LOWERCASE);
const rand2 = secureRandomStringFrom(n, LOWERCASE);
expect(rand1).not.toEqual(rand2);
expect(rand1).not.toEqual(rand2);
expect(rand1).toHaveLength(n);
expect(rand1).toHaveLength(n);
expect(rand1.toLowerCase()).toEqual(rand1);
expect(rand1.toLowerCase()).toEqual(rand1);
},
);
it.each([8, 16, 32])(
"secureRandomStringFrom generates uppercase string of %i characters when given uppercase",
(n: number) => {
const rand1 = secureRandomStringFrom(n, UPPERCASE);
const rand2 = secureRandomStringFrom(n, UPPERCASE);
expect(rand1).not.toEqual(rand2);
expect(rand1).toHaveLength(n);
expect(rand1.toUpperCase()).toEqual(rand1);
},
);
it("throws if given character set less than 2 characters", () => {
expect(() => secureRandomStringFrom(8, "a")).toThrow();
});
it.each([8, 16, 32])("randomUppercaseString generates lowercase string of %i characters", (n: number) => {
const rand1 = randomUppercaseString(n);
const rand2 = randomUppercaseString(n);
it("throws if given character set more than 256 characters", () => {
const charSet = Array.from({ length: 257 }, (_, i) => "a").join("");
expect(rand1).not.toEqual(rand2);
expect(() => secureRandomStringFrom(8, charSet)).toThrow();
});
expect(rand1).toHaveLength(n);
it("throws if given length less than 1", () => {
expect(() => secureRandomStringFrom(0, "abc")).toThrow();
});
expect(rand1.toUpperCase()).toEqual(rand1);
it("throws if given length more than 32768", () => {
expect(() => secureRandomStringFrom(32769, "abc")).toThrow();
});
it("asks for more entropy if given entropy is unusable", () => {
// This is testing the internal implementation details of the function rather
// than strictly the public API. The intention is to have some assertion that
// the rejection sampling to make the distribution even over all possible characters
// is doing what it's supposed to do.
// mock once to fill with 255 the first time: 255 should be unusable because
// we give 10 possible characters below and 256 is not evenly divisible by 10, so
// this should force it to call for more entropy.
jest.spyOn(globalThis.crypto, "getRandomValues").mockImplementationOnce((arr) => {
if (arr === null) throw new Error("Buffer is null");
new Uint8Array(arr.buffer).fill(255);
return arr;
});
secureRandomStringFrom(8, "0123456789");
expect(globalThis.crypto.getRandomValues).toHaveBeenCalledTimes(2);
});
});
+4 -4
View File
@@ -198,8 +198,8 @@ describe("Relations", function () {
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(targetEvent);
timelineSet.addLiveEvent(relationEvent);
timelineSet.addLiveEvent(targetEvent, { addToState: false });
timelineSet.addLiveEvent(relationEvent, { addToState: false });
await relationsCreated;
}
@@ -212,8 +212,8 @@ describe("Relations", function () {
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(relationEvent);
timelineSet.addLiveEvent(targetEvent);
timelineSet.addLiveEvent(relationEvent, { addToState: false });
timelineSet.addLiveEvent(targetEvent, { addToState: false });
await relationsCreated;
}
+71 -1
View File
@@ -65,6 +65,40 @@ describe("RoomMember", function () {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
expect(url).toEqual(null);
});
it("should return unauthenticated media URL if useAuthentication is not set", function () {
member.events.member = utils.mkEvent({
event: true,
type: "m.room.member",
skey: userA,
room: roomId,
user: userA,
content: {
membership: KnownMembership.Join,
avatar_url: "mxc://flibble/wibble",
},
});
const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false);
// Check for unauthenticated media prefix
expect(url?.indexOf("/_matrix/media/v3/")).not.toEqual(-1);
});
it("should return authenticated media URL if useAuthentication=true", function () {
member.events.member = utils.mkEvent({
event: true,
type: "m.room.member",
skey: userA,
room: roomId,
user: userA,
content: {
membership: KnownMembership.Join,
avatar_url: "mxc://flibble/wibble",
},
});
const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false, true);
// Check for authenticated media prefix
expect(url?.indexOf("/_matrix/client/v1/media/")).not.toEqual(-1);
});
});
describe("setPowerLevelEvent", function () {
@@ -486,7 +520,43 @@ describe("RoomMember", function () {
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alíce"); // it should disambig.
expect(member.name).toEqual("Alíce (@alice:bar)"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
it("should disambiguate a user when their displayname looks like an MXID which isn't theirs", function () {
const joinEvent = utils.mkMembership({
event: true,
mship: KnownMembership.Join,
user: userA,
room: roomId,
name: "@clarissa\u0a83bar",
});
const roomState = {
getStateEvents: function (type: string) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true,
mship: KnownMembership.Join,
room: roomId,
user: userC,
name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function (displayName: string) {
return [userA, userC];
},
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).toEqual("@clarissabar (@alice:bar)"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
-55
View File
@@ -1122,59 +1122,4 @@ describe("RoomState", function () {
).toBeFalsy();
});
});
describe("skipWrongOrderRoomStateInserts", () => {
const idNow = "now";
const idBefore = "before";
const endState = new RoomState(roomId);
const startState = new RoomState(roomId, undefined, true);
let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
const evNow = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
evNow.event.unsigned = { replaces_state: idBefore };
evNow.event.event_id = idNow;
const evBefore = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
const updatedStateWithBefore = jest.fn();
const updatedStateWithNow = jest.fn();
beforeEach(() => {
evBefore.event.event_id = idBefore;
onRoomStateEvent = (event, _state, _lastState) => {
if (event.event.event_id === idNow) {
updatedStateWithNow();
} else if (event.event.event_id === idBefore) {
updatedStateWithBefore();
}
};
endState.on(RoomStateEvent.Events, onRoomStateEvent);
startState.on(RoomStateEvent.Events, onRoomStateEvent);
});
afterEach(() => {
endState.off(RoomStateEvent.Events, onRoomStateEvent);
startState.off(RoomStateEvent.Events, onRoomStateEvent);
updatedStateWithNow.mockReset();
updatedStateWithBefore.mockReset();
});
it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => {
endState.setStateEvents([evNow, evBefore]);
expect(updatedStateWithBefore).not.toHaveBeenCalled();
expect(updatedStateWithNow).toHaveBeenCalled();
});
it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => {
startState.setStateEvents([evBefore, evNow]);
expect(updatedStateWithBefore).toHaveBeenCalled();
expect(updatedStateWithNow).not.toHaveBeenCalled();
});
});
});
+636 -437
View File
File diff suppressed because it is too large Load Diff
+605 -24
View File
@@ -30,6 +30,7 @@ import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import {
AccountDataEvents,
Device,
DeviceVerification,
encodeBase64,
@@ -43,7 +44,7 @@ import {
MemoryCryptoStore,
TypedEventEmitter,
} from "../../../src";
import { mkEvent } from "../../test-utils/test-utils";
import { emitPromise, mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult, IMegolmSessionData } from "../../../src/@types/crypto";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
@@ -60,9 +61,12 @@ import {
EventShieldReason,
ImportRoomKeysOpts,
KeyBackupCheck,
KeyBackupInfo,
VerificationRequest,
} from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { defer } from "../../../src/utils";
import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
@@ -71,6 +75,7 @@ import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
import { RustBackupManager } from "../../../src/rust-crypto/backup.ts";
const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
@@ -572,15 +577,37 @@ describe("RustCrypto", () => {
});
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
testData.TEST_USER_ID,
);
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: {
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
},
},
});
// wait until we know about the other device
rustCrypto.onSyncCompleted({});
await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
const toDeviceEvent = {
type: "m.key.verification.request",
content: {
from_device: "testDeviceId",
from_device: testData.TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: "testTxn",
timestamp: Date.now() - 1000,
},
sender: "@user:id",
sender: testData.TEST_USER_ID,
};
const onEvent = jest.fn();
@@ -705,6 +732,119 @@ describe("RustCrypto", () => {
expect(resetKeyBackup.mock.calls).toHaveLength(2);
});
describe("upload existing key backup key to new 4S store", () => {
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
let secretStorage: ServerSideSecretStorageImpl;
let backupAuthData: any;
let backupAlg: string;
const fetchMock = {
authedRequest: jest.fn().mockImplementation((method, path, query, body) => {
if (path === "/room_keys/version") {
if (method === "POST") {
backupAuthData = body["auth_data"];
backupAlg = body["algorithm"];
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
} else if (method === "GET" && backupAuthData) {
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
}
}
return Promise.resolve({});
}),
};
beforeEach(() => {
backupAuthData = undefined;
backupAlg = "";
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
});
it("bootstrapSecretStorage saves megolm backup key if already cached", async () => {
const rustCrypto = await makeTestRustCrypto(
fetchMock as unknown as MatrixHttpApi<any>,
testData.TEST_USER_ID,
undefined,
secretStorage,
);
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto.resetKeyBackup();
const storeSpy = jest.spyOn(secretStorage, "store");
await rustCrypto.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
expect(storeSpy).toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
});
it("bootstrapSecretStorage doesn't try to save megolm backup key not in cache", async () => {
const mockOlmMachine = {
isBackupEnabled: jest.fn().mockResolvedValue(false),
sign: jest.fn().mockResolvedValue({
asJSON: jest.fn().mockReturnValue("{}"),
}),
saveBackupDecryptionKey: jest.fn(),
crossSigningStatus: jest.fn().mockResolvedValue({
hasMaster: true,
hasSelfSigning: true,
hasUserSigning: true,
}),
exportCrossSigningKeys: jest.fn().mockResolvedValue({
masterKey: "sosecret",
userSigningKey: "secrets",
self_signing_key: "ssshhh",
}),
getBackupKeys: jest.fn().mockResolvedValue({}),
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
} as unknown as OlmMachine;
const rustCrypto = new RustCrypto(
logger,
mockOlmMachine,
fetchMock as unknown as MatrixHttpApi<any>,
TEST_USER,
TEST_DEVICE_ID,
secretStorage,
{} as CryptoCallbacks,
);
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto.resetKeyBackup();
const storeSpy = jest.spyOn(secretStorage, "store");
await rustCrypto.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
expect(storeSpy).not.toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
});
});
it("isSecretStorageReady", async () => {
const mockSecretStorage = {
getDefaultKeyId: jest.fn().mockResolvedValue(null),
@@ -991,23 +1131,41 @@ describe("RustCrypto", () => {
});
it.each([
[undefined, null],
["Encrypted by an unverified user.", EventShieldReason.UNVERIFIED_IDENTITY],
["Encrypted by a device not verified by its owner.", EventShieldReason.UNSIGNED_DEVICE],
[undefined, undefined, null],
[
"Encrypted by an unverified user.",
RustSdkCryptoJs.ShieldStateCode.UnverifiedIdentity,
EventShieldReason.UNVERIFIED_IDENTITY,
],
[
"Encrypted by a device not verified by its owner.",
RustSdkCryptoJs.ShieldStateCode.UnsignedDevice,
EventShieldReason.UNSIGNED_DEVICE,
],
[
"The authenticity of this encrypted message can't be guaranteed on this device.",
RustSdkCryptoJs.ShieldStateCode.AuthenticityNotGuaranteed,
EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
],
["Encrypted by an unknown or deleted device.", EventShieldReason.UNKNOWN_DEVICE],
["bloop", EventShieldReason.UNKNOWN],
])("gets the right shield reason (%s)", async (rustReason, expectedReason) => {
[
"Encrypted by an unknown or deleted device.",
RustSdkCryptoJs.ShieldStateCode.UnknownDevice,
EventShieldReason.UNKNOWN_DEVICE,
],
["Not encrypted.", RustSdkCryptoJs.ShieldStateCode.SentInClear, EventShieldReason.SENT_IN_CLEAR],
[
"Encrypted by a previously-verified user who is no longer verified.",
RustSdkCryptoJs.ShieldStateCode.VerificationViolation,
EventShieldReason.VERIFICATION_VIOLATION,
],
])("gets the right shield reason (%s)", async (rustReason, rustCode, expectedReason) => {
// suppress the warning from the unknown shield reason
jest.spyOn(console, "warn").mockImplementation(() => {});
const mockEncryptionInfo = {
shieldState: jest
.fn()
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, message: rustReason }),
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, code: rustCode, message: rustReason }),
} as unknown as RustSdkCryptoJs.EncryptionInfo;
olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo);
@@ -1378,13 +1536,58 @@ describe("RustCrypto", () => {
describe("pinCurrentIdentity", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
beforeEach(async () => {
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
const secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
TEST_USER,
TEST_DEVICE_ID,
secretStorage,
);
});
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@other_user:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
);
});
it("throws an error for our own user", async () => {
jest.useRealTimers();
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
const e2eKeyResponder = new E2EKeyResponder("http://server");
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
status: 200,
body: {},
});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
status: 200,
body: {},
});
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
await expect(rustCrypto.pinCurrentUserIdentity(TEST_USER)).rejects.toThrow(
"Cannot pin identity of own user",
);
});
});
describe("withdraw verification", () => {
function createTestSetup(): { olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>; rustCrypto: RustCrypto } {
const olmMachine = {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
const rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
@@ -1393,21 +1596,40 @@ describe("RustCrypto", () => {
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
return { olmMachine, rustCrypto };
}
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
const { rustCrypto } = createTestSetup();
await expect(rustCrypto.withdrawVerificationRequirement("@alice:example.com")).rejects.toThrow(
"Cannot withdraw verification of unknown user",
);
});
it("throws an error for our own user", async () => {
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
it("Calls withdraw for other identity", async () => {
const { olmMachine, rustCrypto } = createTestSetup();
const identity = {
withdrawVerification: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OtherUserIdentity>;
olmMachine.getIdentity.mockResolvedValue(identity);
await rustCrypto.withdrawVerificationRequirement("@bob:example.com");
expect(identity.withdrawVerification).toHaveBeenCalled();
});
it("Calls withdraw for own identity", async () => {
const { olmMachine, rustCrypto } = createTestSetup();
const ownIdentity = {
withdrawVerification: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OwnUserIdentity>;
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of own user",
);
await rustCrypto.withdrawVerificationRequirement("@alice:example.com");
expect(ownIdentity.withdrawVerification).toHaveBeenCalled();
});
});
@@ -1526,6 +1748,20 @@ describe("RustCrypto", () => {
failures: 1,
});
});
describe("getKeyBackupInfo", () => {
it("should return the current key backup info", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
await expect(rustCrypto.getKeyBackupInfo()).resolves.toStrictEqual(testData.SIGNED_BACKUP_DATA);
});
it("should return null if not available", async () => {
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
await expect(rustCrypto.getKeyBackupInfo()).resolves.toBeNull();
});
});
});
describe("device dehydration", () => {
@@ -1554,6 +1790,297 @@ describe("RustCrypto", () => {
});
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
});
it("should load the dehydration key from SSSS if available", async () => {
fetchMock.config.overwriteRoutes = true;
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
const secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
// Create a RustCrypto to set up device dehydration.
const e2eKeyReceiver1 = new E2EKeyReceiver("http://server");
const e2eKeyResponder1 = new E2EKeyResponder("http://server");
e2eKeyResponder1.addKeyReceiver(TEST_USER, e2eKeyReceiver1);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
status: 200,
body: {},
});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
status: 200,
body: {},
});
const rustCrypto1 = await makeTestRustCrypto(makeMatrixHttpApi(), TEST_USER, TEST_DEVICE_ID, secretStorage);
// dehydration requires secret storage and cross signing
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto1.bootstrapCrossSigning({ setupNewCrossSigning: true });
await rustCrypto1.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
// we need to process a sync so that the OlmMachine will upload keys
await rustCrypto1.preprocessToDeviceMessages([]);
await rustCrypto1.onSyncCompleted({});
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
let dehydratedDeviceBody: any;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
return {};
});
await rustCrypto1.startDehydration();
await rustCrypto1.stop();
// Create another RustCrypto, using the same SecretStorage, to
// rehydrate the device.
const e2eKeyReceiver2 = new E2EKeyReceiver("http://server");
const e2eKeyResponder2 = new E2EKeyResponder("http://server");
e2eKeyResponder2.addKeyReceiver(TEST_USER, e2eKeyReceiver2);
const rustCrypto2 = await makeTestRustCrypto(
makeMatrixHttpApi(),
TEST_USER,
"ANOTHERDEVICE",
secretStorage,
);
// dehydration requires secret storage and cross signing
await rustCrypto2.bootstrapCrossSigning({ setupNewCrossSigning: true });
// we need to process a sync so that the OlmMachine will upload keys
await rustCrypto2.preprocessToDeviceMessages([]);
await rustCrypto2.onSyncCompleted({});
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
});
fetchMock.post(
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
{
events: [],
next_batch: "token",
},
);
// We check that a RehydrationCompleted event gets emitted, which
// means that the device was successfully rehydrated.
const rehydrationCompletedPromise = emitPromise(rustCrypto2, CryptoEvent.RehydrationCompleted);
await rustCrypto2.startDehydration();
await rehydrationCompletedPromise;
await rustCrypto2.stop();
});
describe("start dehydration options", () => {
let rustCrypto: RustCrypto;
let secretStorage: ServerSideSecretStorageImpl;
let dehydratedDeviceInfo: Record<string, any> | undefined;
// Function that is called when `GET /dehydrated_device` is called
// (i.e. when we try to rehydrate a device)
const getDehydratedDeviceMock = jest.fn(() => {
if (dehydratedDeviceInfo) {
return {
status: 200,
body: dehydratedDeviceInfo,
};
} else {
return {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
};
}
});
// Function that is called when `PUT /dehydrated_device` is called
// (i.e. when we create a new dehydrated device)
const putDehydratedDeviceMock = jest.fn((path, opts) => {
const content = JSON.parse(opts.body as string);
dehydratedDeviceInfo = {
device_id: content.device_id,
device_data: content.device_data,
};
return {
status: 200,
body: {
device_id: content.device_id,
},
};
});
beforeEach(async () => {
// Set up a RustCrypto object with secret storage and cross-signing.
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
const e2eKeyResponder = new E2EKeyResponder("http://server");
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
status: 200,
body: {},
});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
status: 200,
body: {},
});
rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), TEST_USER, TEST_DEVICE_ID, secretStorage);
// dehydration requires secret storage and cross signing
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
await rustCrypto.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
// we need to process a sync so that the OlmMachine will upload keys
await rustCrypto.preprocessToDeviceMessages([]);
await rustCrypto.onSyncCompleted({});
// set up mocks needed for device dehydration
dehydratedDeviceInfo = undefined;
fetchMock.get(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
getDehydratedDeviceMock,
);
fetchMock.put(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
putDehydratedDeviceMock,
);
fetchMock.post(/_matrix\/client\/unstable\/org.matrix.msc3814.v1\/dehydrated_device\/.*\/events/, {
status: 200,
body: {
events: [],
next_batch: "foo",
},
});
getDehydratedDeviceMock.mockClear();
putDehydratedDeviceMock.mockClear();
});
afterEach(() => {
rustCrypto.stop();
});
// Several tests require a dehydrated device and dehydration key
// already set up.
async function setUpInitialDehydratedDevice() {
await rustCrypto.startDehydration();
getDehydratedDeviceMock.mockClear();
putDehydratedDeviceMock.mockClear();
return await secretStorage.get("org.matrix.msc3814");
}
it("should create a new key and dehydrate a device when no options given", async () => {
// With the default options, when we don't have an existing key ...
await rustCrypto.startDehydration();
// ... we create a new dehydration key ...
expect(await secretStorage.get("org.matrix.msc3814")).toBeTruthy();
// ... and create a new dehydrated device.
expect(putDehydratedDeviceMock).toHaveBeenCalled();
});
it("should rehydrate a device if available and keep existing key when no options given", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If we already have a dehydration key and dehydrated device...
await rustCrypto.startDehydration();
// ... we should fetch the device to rehydrate it ...
expect(getDehydratedDeviceMock).toHaveBeenCalled();
// ... create a new dehydrated device ...
expect(putDehydratedDeviceMock).toHaveBeenCalled();
// ... and keep the same dehydration key.
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
});
it("should do nothing if onlyIfKeyCached is true and we have no key cached", async () => {
// Since there is no key cached, so should do nothing. i.e. it
// should not make any HTTP requests and should not create a new key.
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
expect(putDehydratedDeviceMock).not.toHaveBeenCalled();
expect(await secretStorage.get("org.matrix.msc3814")).toBeFalsy();
});
it("should start dehydration when onlyIfKeyCached is true, and we have a cached key", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If `onlyIfKeyCached` is `true`, and we already have have a
// key, we should behave the same as if no options were given.
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
expect(getDehydratedDeviceMock).toHaveBeenCalled();
expect(putDehydratedDeviceMock).toHaveBeenCalled();
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
});
it("should not rehydrate if rehydrate is set to false", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If `rehydrate` is set to `false` ...
await rustCrypto.startDehydration({ rehydrate: false });
// ... we should not try to rehydrate ...
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
// ... but we should still create a new dehydrated device ...
expect(putDehydratedDeviceMock).toHaveBeenCalled();
// ... and we should keep the same dehydration key.
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
});
it("should create a new key if createNewKey is set to true", async () => {
const origDehydrationKey = await setUpInitialDehydratedDevice();
// If `createNewKey` is set to `true` ...
await rustCrypto.startDehydration({ createNewKey: true });
// ... we should rehydrate and dehydrate as normal ...
expect(getDehydratedDeviceMock).toHaveBeenCalled();
expect(putDehydratedDeviceMock).toHaveBeenCalled();
// ... and we should create a new dehydration key.
expect(await secretStorage.get("org.matrix.msc3814")).not.toEqual(origDehydrationKey);
});
});
});
describe("import & export secrets bundle", () => {
@@ -1711,6 +2238,58 @@ describe("RustCrypto", () => {
);
});
});
describe("resetEncryption", () => {
let secretStorage: ServerSideSecretStorage;
beforeEach(() => {
secretStorage = {
setDefaultKeyId: jest.fn(),
hasKey: jest.fn().mockResolvedValue(false),
getKey: jest.fn().mockResolvedValue(null),
} as unknown as ServerSideSecretStorage;
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
});
it("reset should reset 4S, backup and cross-signing", async () => {
// When we will delete the key backup
let backupIsDeleted = false;
fetchMock.delete("path:/_matrix/client/v3/room_keys/version/1", () => {
backupIsDeleted = true;
return {};
});
// If the backup is deleted, we will return an empty object
fetchMock.get("path:/_matrix/client/v3/room_keys/version", () => {
return backupIsDeleted ? {} : testData.SIGNED_BACKUP_DATA;
});
// A new key backup should be created after the reset
let newKeyBackupInfo!: KeyBackupInfo;
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (res, options) => {
newKeyBackupInfo = JSON.parse(options.body as string);
return { version: "2" };
});
// We consider the key backup as trusted
jest.spyOn(RustBackupManager.prototype, "isKeyBackupTrusted").mockResolvedValue({
trusted: true,
matchesDecryptionKey: true,
});
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
// We have a key backup
expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull();
const authUploadDeviceSigningKeys = jest.fn();
await rustCrypto.resetEncryption(authUploadDeviceSigningKeys);
// A new key backup should be created
expect(newKeyBackupInfo.auth_data).toBeTruthy();
// The new cross signing keys should be uploaded
expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function));
});
});
});
/** Build a MatrixHttpApi instance */
@@ -1757,11 +2336,13 @@ class DummyAccountDataClient
super();
}
public async getAccountDataFromServer<T extends Record<string, any>>(eventType: string): Promise<T | null> {
public async getAccountDataFromServer<K extends keyof AccountDataEvents>(
eventType: K,
): Promise<AccountDataEvents[K] | null> {
const ret = this.storage.get(eventType);
if (eventType) {
return ret as T;
return ret;
} else {
return null;
}
@@ -19,6 +19,18 @@ import {
secretStorageContainsCrossSigningKeys,
} from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
import { SecretInfo } from "../../../src/secret-storage.ts";
declare module "../../../src/@types/event" {
interface SecretStorageAccountDataEvents {
secretA: SecretInfo;
secretB: SecretInfo;
secretC: SecretInfo;
secretD: SecretInfo;
secretE: SecretInfo;
Unknown: SecretInfo;
}
}
describe("secret-storage", () => {
describe("secretStorageContainsCrossSigningKeys", () => {
+102 -9
View File
@@ -23,10 +23,20 @@ import {
SecretStorageCallbacks,
SecretStorageKeyDescriptionAesV1,
SecretStorageKeyDescriptionCommon,
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
trimTrailingEquals,
} from "../../src/secret-storage";
import { randomString } from "../../src/randomstring";
import { secureRandomString } from "../../src/randomstring";
import { SecretInfo } from "../../src/secret-storage.ts";
import { AccountDataEvents, ClientEvent, MatrixEvent, TypedEventEmitter } from "../../src";
import { defer, IDeferred } from "../../src/utils";
declare module "../../src/@types/event" {
interface SecretStorageAccountDataEvents {
mysecret: SecretInfo;
}
}
describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
@@ -117,9 +127,11 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.key.my_key") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
@@ -135,11 +147,13 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.default_key") {
return { key: "default_key_id" } as unknown as T;
return { key: "default_key_id" } as any;
} else if (eventType === "m.secret_storage.key.default_key_id") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
@@ -165,7 +179,7 @@ describe("ServerSideSecretStorageImpl", function () {
it("should return true for a correct key check", async function () {
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
const myKey = new TextEncoder().encode(randomString(32));
const myKey = new TextEncoder().encode(secureRandomString(32));
const { iv, mac } = await calculateKeyCheck(myKey);
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
@@ -236,9 +250,11 @@ describe("ServerSideSecretStorageImpl", function () {
// stub out getAccountData to return a key with an unknown algorithm
const storedKey = { algorithm: "badalg" } as SecretStorageKeyDescriptionCommon;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.key.keyid") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
@@ -259,6 +275,78 @@ describe("ServerSideSecretStorageImpl", function () {
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("unknown algorithm"));
});
});
describe("setDefaultKeyId", function () {
let secretStorage: ServerSideSecretStorage;
let accountDataAdapter: Mocked<AccountDataClient>;
let accountDataPromise: IDeferred<void>;
beforeEach(() => {
accountDataAdapter = mockAccountDataClient();
accountDataPromise = defer();
accountDataAdapter.setAccountData.mockImplementation(() => {
accountDataPromise.resolve();
return Promise.resolve({});
});
secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
});
it("should set the default key id", async function () {
const setDefaultPromise = secretStorage.setDefaultKeyId("keyId");
await accountDataPromise.promise;
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", {
key: "keyId",
});
accountDataAdapter.emit(
ClientEvent.AccountData,
new MatrixEvent({
type: "m.secret_storage.default_key",
content: { key: "keyId" },
}),
);
await setDefaultPromise;
});
it("should set the default key id with a null key id", async function () {
const setDefaultPromise = secretStorage.setDefaultKeyId(null);
await accountDataPromise.promise;
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", {});
accountDataAdapter.emit(
ClientEvent.AccountData,
new MatrixEvent({
type: "m.secret_storage.default_key",
content: {},
}),
);
await setDefaultPromise;
});
});
describe("getDefaultKeyId", function () {
it("should return null when there is no key", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
expect(await secretStorage.getDefaultKeyId()).toBe(null);
});
it("should return the key id when there is a key", async function () {
const accountDataAdapter = mockAccountDataClient();
accountDataAdapter.getAccountDataFromServer.mockResolvedValue({ key: "keyId" });
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
expect(await secretStorage.getDefaultKeyId()).toBe("keyId");
});
it("should return null when an empty object is in the account data", async function () {
const accountDataAdapter = mockAccountDataClient();
accountDataAdapter.getAccountDataFromServer.mockResolvedValue({});
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
expect(await secretStorage.getDefaultKeyId()).toBe(null);
});
});
});
describe("trimTrailingEquals", () => {
@@ -277,8 +365,13 @@ describe("trimTrailingEquals", () => {
});
function mockAccountDataClient(): Mocked<AccountDataClient> {
const eventEmitter = new TypedEventEmitter();
return {
getAccountDataFromServer: jest.fn().mockResolvedValue(null),
setAccountData: jest.fn().mockResolvedValue({}),
on: eventEmitter.on.bind(eventEmitter),
off: eventEmitter.off.bind(eventEmitter),
removeListener: eventEmitter.removeListener.bind(eventEmitter),
emit: eventEmitter.emit.bind(eventEmitter),
} as unknown as Mocked<AccountDataClient>;
}
+153 -10
View File
@@ -17,8 +17,10 @@ limitations under the License.
import { ReceiptType } from "../../src/@types/read_receipts";
import {
IJoinedRoom,
Category,
IInvitedRoom,
IInviteState,
IJoinedRoom,
IKnockedRoom,
IKnockState,
ILeftRoom,
@@ -27,7 +29,6 @@ import {
IStrippedState,
ISyncResponse,
SyncAccumulator,
IInviteState,
} from "../../src/sync-accumulator";
import { IRoomSummary } from "../../src";
import * as utils from "../test-utils/test-utils";
@@ -85,6 +86,7 @@ describe("SyncAccumulator", function () {
// technically cheating since we also cheekily pre-populate keys we
// know that the sync accumulator will pre-populate.
// It isn't 100% transitive.
const events = [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)];
const res = {
next_batch: "abc",
rooms: {
@@ -92,18 +94,17 @@ describe("SyncAccumulator", function () {
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)],
},
summary: {
"account_data": { events: [] },
"ephemeral": { events: [] },
"unread_notifications": {},
"org.matrix.msc4222.state_after": { events },
"state": { events },
"summary": {
"m.heroes": undefined,
"m.joined_member_count": undefined,
"m.invited_member_count": undefined,
},
timeline: {
"timeline": {
events: [msg("alice", "hi")],
prev_batch: "something",
},
@@ -882,6 +883,147 @@ describe("SyncAccumulator", function () {
).not.toBeUndefined();
});
});
describe("msc4222", () => {
it("should accumulate state_after events", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 1",
},
skey: "",
}) as IStateEvent,
],
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).toEqual("Room 1");
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 2",
},
skey: "",
}) as IStateEvent,
],
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).toEqual("Room 2");
});
it("should ignore state events in timeline", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [],
},
"timeline": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 1",
},
skey: "",
}) as IStateEvent,
],
prev_batch: "something",
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).not.toEqual("Room 1");
});
it("should not rewind state_after to start of timeline in toJSON", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
"timeline": {
events: initState.events,
prev_batch: null,
},
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
const joinEvent = member("alice", KnownMembership.Join);
joinEvent.unsigned = { prev_content: initState.events[0].content, prev_sender: initState.events[0].sender };
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [joinEvent],
},
"timeline": {
events: [joinEvent],
prev_batch: "something",
},
}),
);
const roomData = sa.getJSON().roomsData[Category.Join]["!foo:bar"];
expect(roomData.state?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
KnownMembership.Knock,
);
expect(
roomData["org.matrix.msc4222.state_after"]?.events.find((e) => e.type === "m.room.member")?.content
.membership,
).toEqual(KnownMembership.Join);
expect(roomData.timeline?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
KnownMembership.Join,
);
});
});
});
function syncSkeleton(
@@ -961,5 +1103,6 @@ function member(localpart: string, membership: Membership) {
state_key: "@" + localpart + ":localhost",
sender: "@" + localpart + ":localhost",
type: "m.room.member",
unsigned: {},
};
}
+2 -2
View File
@@ -15,12 +15,12 @@ limitations under the License.
*/
import { IEvent } from "../../src";
import { randomString } from "../../src/randomstring";
import { secureRandomString } from "../../src/randomstring";
import { getRelationsThreadFilter } from "../../src/thread-utils";
function makeEvent(relatesToEvent: string, relType: string): Partial<IEvent> {
return {
event_id: randomString(10),
event_id: secureRandomString(10),
type: "m.room.message",
content: {
"msgtype": "m.text",
+3 -3
View File
@@ -62,7 +62,7 @@ function addEventsToTimeline(timeline: EventTimeline, numEvents: number, toStart
user: USER_ID,
event: true,
}),
{ toStartOfTimeline },
{ toStartOfTimeline, addToState: false },
);
}
}
@@ -451,8 +451,8 @@ describe("TimelineWindow", function () {
const liveEvents = createEvents(5);
const [, , e3, e4, e5] = oldEvents;
const [, e7, e8, e9, e10] = liveEvents;
room.addLiveEvents(liveEvents);
room.addEventsToTimeline(oldEvents, true, oldTimeline);
room.addLiveEvents(liveEvents, { addToState: false });
room.addEventsToTimeline(oldEvents, true, false, oldTimeline);
// And 2 windows over the timelines in this room
const oldWindow = new TimelineWindow(mockClient, timelineSet);
+16
View File
@@ -32,6 +32,7 @@ import {
MapWithDefault,
globToRegexp,
escapeRegExp,
removeHiddenChars,
} from "../../src/utils";
import { logger } from "../../src/logger";
import { mkMessage } from "../test-utils/test-utils";
@@ -740,4 +741,19 @@ describe("utils", function () {
expect(result).toMatchInlineSnapshot(`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`);
});
});
describe("removeHiddenChars", () => {
it.each([
["various width spaces U+2000 - U+200D", "\u2000\u200D", ""],
["LTR and RTL marks U+200E and U+200F", "\u200E\u200F", ""],
["LTR/RTL and other directional formatting marks U+202A - U+202F", "\u202A\u202F", ""],
["Arabic Letter RTL mark U+061C", "\u061C", ""],
["Combining characters U+0300 - U+036F", "\u3000\u036F", ""],
["Zero width no-break space (BOM) U+FEFF", "\uFEFF", ""],
["Blank/invisible characters (U2800, U2062-U2063)", "\u2800\u2062\u2063", ""],
["Zero Width Non Joiner", "", ""],
])("should strip invisible characters: %s", (_, input, expected) => {
expect(removeHiddenChars(input)).toBe(expected);
});
});
});
+14 -11
View File
@@ -1430,7 +1430,7 @@ describe("Group Call", function () {
let client: MatrixClient;
beforeEach(() => {
client = new MatrixClient({ baseUrl: "base_url" });
client = new MatrixClient({ baseUrl: "base_url", userId: "my_user_id" });
jest.spyOn(client, "sendStateEvent").mockResolvedValue({} as any);
});
@@ -1566,16 +1566,19 @@ describe("Group Call", function () {
async (roomId, eventType, content, stateKey) => {
const eventId = `$${Math.random()}`;
if (roomId === room.roomId) {
room.addLiveEvents([
new MatrixEvent({
event_id: eventId,
type: eventType,
room_id: roomId,
sender: FAKE_USER_ID_2,
content,
state_key: stateKey,
}),
]);
room.addLiveEvents(
[
new MatrixEvent({
event_id: eventId,
type: eventType,
room_id: roomId,
sender: FAKE_USER_ID_2,
content,
state_key: stateKey,
}),
],
{ addToState: true },
);
}
return { event_id: eventId };
},
+39 -9
View File
@@ -35,11 +35,7 @@ import {
SpaceChildEventContent,
SpaceParentEventContent,
} from "./state_events.ts";
import {
ExperimentalGroupCallRoomMemberState,
IGroupCallRoomMemberState,
IGroupCallRoomState,
} from "../webrtc/groupCall.ts";
import { IGroupCallRoomMemberState, IGroupCallRoomState } from "../webrtc/groupCall.ts";
import { MSC3089EventContent } from "../models/MSC3089Branch.ts";
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon.ts";
import { XOR } from "./common.ts";
@@ -58,6 +54,10 @@ import {
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts";
import { SessionMembershipData } from "../matrixrtc/CallMembership.ts";
import { LocalNotificationSettings } from "./local_notifications.ts";
import { IPushRules } from "./PushRules.ts";
import { SecretInfo, SecretStorageKeyDescription } from "../secret-storage.ts";
import { POLICIES_ACCOUNT_EVENT_TYPE } from "../models/invites-ignorer-types.ts";
export enum EventType {
// Room state events
@@ -357,10 +357,7 @@ export interface StateEvents {
// MSC3401
[EventType.GroupCallPrefix]: IGroupCallRoomState;
[EventType.GroupCallMemberPrefix]: XOR<
XOR<IGroupCallRoomMemberState, ExperimentalGroupCallRoomMemberState>,
XOR<SessionMembershipData, {}>
>;
[EventType.GroupCallMemberPrefix]: XOR<IGroupCallRoomMemberState, XOR<SessionMembershipData, {}>>;
// MSC3089
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
@@ -368,3 +365,36 @@ export interface StateEvents {
// MSC3672
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
}
/**
* Mapped type from event type to content type for all specified global account_data events.
*/
export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[EventType.PushRules]: IPushRules;
[EventType.Direct]: { [userId: string]: string[] };
[EventType.IgnoredUserList]: { [userId: string]: {} };
"m.secret_storage.default_key": { key: string };
// Flag set by the rust SDK (Element X) and also used by us to mark that the user opted out of backup
// (I don't know why it's m.org.matrix...)
"m.org.matrix.custom.backup_disabled": { disabled: boolean };
"m.identity_server": { base_url: string | null };
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
// Invites-ignorer events
[POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any };
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
}
/**
* Mapped type from event type to content type for all specified global events encrypted by secret storage.
*
* See https://spec.matrix.org/v1.13/client-server-api/#msecret_storagev1aes-hmac-sha2-1
*/
export interface SecretStorageAccountDataEvents {
"m.megolm_backup.v1": SecretInfo;
"m.cross_signing.master": SecretInfo;
"m.cross_signing.self_signing": SecretInfo;
"m.cross_signing.user_signing": SecretInfo;
"org.matrix.msc3814": SecretInfo;
}
+21 -1
View File
@@ -65,6 +65,26 @@ declare global {
interface Navigator {
// We check for the webkit-prefixed getUserMedia to detect if we're
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
webkitGetUserMedia?: DummyInterfaceWeShouldntBeUsingThis;
}
export interface Uint8ArrayToBase64Options {
alphabet?: "base64" | "base64url";
omitPadding?: boolean;
}
interface Uint8Array {
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
toBase64?(options?: Uint8ArrayToBase64Options): string;
}
export interface Uint8ArrayFromBase64Options {
alphabet?: "base64"; // Our fallback code only handles base64.
lastChunkHandling?: "loose"; // Our fallback code doesn't support other handling at this time.
}
interface Uint8ArrayConstructor {
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
fromBase64?(base64: string, options?: Uint8ArrayFromBase64Options): Uint8Array;
}
}
+5
View File
@@ -20,6 +20,11 @@ declare module "@matrix-org/matrix-sdk-crypto-wasm" {
interface OlmMachine {
importSecretsBundle(bundle: RustSdkCryptoJs.SecretsBundle): Promise<void>;
exportSecretsBundle(): Promise<RustSdkCryptoJs.SecretsBundle>;
importCrossSigningKeys(
master_key?: string,
self_signing_key?: string,
user_signing_key?: string,
): Promise<RustSdkCryptoJs.CrossSigningStatus>;
}
interface SecretsBundle {
+36
View File
@@ -65,3 +65,39 @@ export interface IUsageLimit {
// eslint-disable-next-line camelcase
admin_contact?: string;
}
/**
* A policy name & url in a specific internationalisation
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_internationalised-policy
*/
export interface InternationalisedPolicy {
name: string;
url: string;
}
/**
* A versioned policy with internationalised variants
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_policy-object
*/
export interface Policy {
/**
* The version for the policy.
* There are no requirements on what this might be and could be alpha, semantically versioned, or arbitrary.
*/
version: string;
/**
* The policy information for the specified language.
* @remarks the type has to include a union with string due to limitations in the type system.
*/
[lang: string]: InternationalisedPolicy | string;
}
/**
* Response from the Terms API for Identity servers
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms
*/
export interface Terms {
policies: {
[policyName: string]: Policy;
};
}
+1 -1
View File
@@ -90,7 +90,7 @@ export interface RoomNameEventContent {
}
export interface RoomTopicEventContent {
topic: string;
topic: string | undefined | null;
}
export interface RoomAvatarEventContent {
+7 -5
View File
@@ -13,9 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EitherAnd } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue.ts";
import { IMessageRendering } from "./extensible_events.ts";
@@ -52,12 +49,17 @@ export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.topic");
*/
export type MTopicContent = IMessageRendering[];
type MTopicStable = { [M_TOPIC.altName]: MTopicContent };
type MTopicUnstable = { [M_TOPIC.name]: MTopicContent };
/**
* The event definition for an m.topic event (in content)
*/
export type MTopicEvent = EitherAnd<{ [M_TOPIC.name]: MTopicContent }, { [M_TOPIC.altName]: MTopicContent }>;
export type MTopicEvent = (MTopicStable & MTopicUnstable) | MTopicStable | MTopicUnstable;
/**
* The event content for an m.room.topic event
*/
export type MRoomTopicEventContent = { topic: string } & MTopicEvent;
export type MRoomTopicEventContent = {
topic: string | null | undefined;
} & Partial<MTopicEvent>;
+1 -1
View File
@@ -130,7 +130,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise<ClientConfig> {
public static async fromDiscoveryConfig(wellknown?: IClientWellKnown): Promise<ClientConfig> {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
+38 -40
View File
@@ -18,30 +18,32 @@ limitations under the License.
* Base64 encoding and decoding utilities
*/
function toBase64(uint8Array: Uint8Array, options: Uint8ArrayToBase64Options): string {
if (typeof uint8Array.toBase64 === "function") {
// Currently this is only supported in Firefox,
// but we match the options in the hope in the future we can rely on it for all environments.
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
return uint8Array.toBase64(options);
}
let base64 = btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
if (options.omitPadding) {
base64 = base64.replace(/={1,2}$/, "");
}
if (options.alphabet === "base64url") {
base64 = base64.replace(/\+/g, "-").replace(/\//g, "_");
}
return base64;
}
/**
* Encode a typed array of uint8 as base64.
* @param uint8Array - The data to encode.
* @returns The base64.
*/
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
// A brief note on the state of base64 encoding in Javascript.
// As of 2023, there is still no common native impl between both browsers and
// node. Older Webpack provides an impl for Buffer and there is a polyfill class
// for it. There are also plenty of pure js impls, eg. base64-js which has 2336
// dependents at current count. Using this would probably be fine although it's
// a little under-docced and run by an individual. The node impl works fine,
// the browser impl works but predates Uint8Array and so only uses strings.
// Right now, switching between native (or polyfilled) impls like this feels
// like the least bad option, but... *shrugs*.
if (typeof Buffer === "function") {
return Buffer.from(uint8Array).toString("base64");
} else if (typeof btoa === "function" && uint8Array instanceof Uint8Array) {
// ArrayBuffer is a node concept so the param should always be a Uint8Array on
// the browser. We need to check because ArrayBuffers don't have reduce.
return btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
} else {
throw new Error("No base64 impl found!");
}
export function encodeBase64(uint8Array: Uint8Array): string {
return toBase64(uint8Array, { alphabet: "base64", omitPadding: false });
}
/**
@@ -49,8 +51,8 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeBase64(uint8Array).replace(/={1,2}$/, "");
export function encodeUnpaddedBase64(uint8Array: Uint8Array): string {
return toBase64(uint8Array, { alphabet: "base64", omitPadding: true });
}
/**
@@ -58,8 +60,19 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeUnpaddedBase64(uint8Array).replace(/\+/g, "-").replace(/\//g, "_");
export function encodeUnpaddedBase64Url(uint8Array: Uint8Array): string {
return toBase64(uint8Array, { alphabet: "base64url", omitPadding: true });
}
function fromBase64(base64: string, options: Uint8ArrayFromBase64Options): Uint8Array {
if (typeof Uint8Array.fromBase64 === "function") {
// Currently this is only supported in Firefox,
// but we match the options in the hope in the future we can rely on it for all environments.
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
return Uint8Array.fromBase64(base64, options);
}
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
/**
@@ -68,21 +81,6 @@ export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): s
* @returns The decoded data.
*/
export function decodeBase64(base64: string): Uint8Array {
// See encodeBase64 for a short treatise on base64 en/decoding in JS
if (typeof Buffer === "function") {
return Buffer.from(base64, "base64");
} else if (typeof atob === "function") {
const itFunc = function* (): Generator<number> {
const decoded = atob(
// built-in atob doesn't support base64url: convert so we support either
base64.replace(/-/g, "+").replace(/_/g, "/"),
);
for (let i = 0; i < decoded.length; ++i) {
yield decoded.charCodeAt(i);
}
};
return Uint8Array.from(itFunc());
} else {
throw new Error("No base64 impl found!");
}
// The function requires us to select an alphabet, but we don't know if base64url was used so we convert.
return fromBase64(base64.replace(/-/g, "+").replace(/_/g, "/"), { alphabet: "base64", lastChunkHandling: "loose" });
}
+193 -73
View File
@@ -46,7 +46,6 @@ import { noUnsafeEventProps, QueryDict, replaceParam, safeSet, sleep } from "./u
import { Direction, EventTimeline } from "./models/event-timeline.ts";
import { IActionsObject, PushProcessor } from "./pushprocessor.ts";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery.ts";
import * as olmlib from "./crypto/olmlib.ts";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64.ts";
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice.ts";
import { IOlmDevice } from "./crypto/algorithms/megolm.ts";
@@ -80,7 +79,6 @@ import {
CryptoEventHandlerMap as LegacyCryptoEventHandlerMap,
fixBackupKey,
ICheckOwnCrossSigningTrustOpts,
ICryptoCallbacks,
IRoomKeyRequestBody,
isCryptoAvailable,
} from "./crypto/index.ts";
@@ -138,6 +136,7 @@ import {
UpdateDelayedEventAction,
} from "./@types/requests.ts";
import {
AccountDataEvents,
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
@@ -158,10 +157,11 @@ import {
IdServerUnbindResult,
JoinRule,
Preset,
Terms,
Visibility,
} from "./@types/partials.ts";
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper.ts";
import { randomString } from "./randomstring.ts";
import { secureRandomString } from "./randomstring.ts";
import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup.ts";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace.ts";
import { ISignatures } from "./@types/signed.ts";
@@ -229,10 +229,12 @@ import {
ImportRoomKeysOpts,
CryptoEvent,
CryptoEventHandlerMap,
CryptoCallbacks,
} from "./crypto-api/index.ts";
import { DeviceInfoMap } from "./crypto/DeviceList.ts";
import {
AddSecretStorageKeyOpts,
SecretStorageKey,
SecretStorageKeyDescription,
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
@@ -246,12 +248,16 @@ import { ImageInfo } from "./@types/media.ts";
import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts";
import { sha256 } from "./digest.ts";
import { keyFromAuthData } from "./common-crypto/key-passphrase.ts";
import { discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig, validateAuthMetadataAndKeys } from "./oidc/index.ts";
export type Store = IStore;
export type ResetTimelineCallback = (roomId: string) => boolean;
const SCROLLBACK_DELAY_MS = 3000;
/**
* @deprecated Not supported for Rust Cryptography.
*/
export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
@@ -286,12 +292,16 @@ export interface ICreateClientOpts {
store?: Store;
/**
* A store to be used for end-to-end crypto session data. If not specified,
* end-to-end crypto will be disabled. The `createClient` helper will create
* a default store if needed. Calls the factory supplied to
* A store to be used for end-to-end crypto session data.
* The `createClient` helper will create a default store if needed. Calls the factory supplied to
* {@link setCryptoStoreFactory} if unspecified; or if no factory has been
* specified, uses a default implementation (indexeddb in the browser,
* in-memory otherwise).
*
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}),
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
* data can be migrated from the legacy store.
*/
cryptoStore?: CryptoStore;
@@ -383,7 +393,7 @@ export interface ICreateClientOpts {
*
* This must be set to the same value every time the client is initialised for the same device.
*
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}),
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}),
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
* data can be migrated from the legacy store.
@@ -433,7 +443,7 @@ export interface ICreateClientOpts {
/**
* Crypto callbacks provided by the application
*/
cryptoCallbacks?: ICryptoCallbacks;
cryptoCallbacks?: CryptoCallbacks;
/**
* Method to generate room names for empty rooms and rooms names based on membership.
@@ -1216,7 +1226,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private readonly logger: Logger;
public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
public olmVersion: [number, number, number] | null = null; // populated after initCrypto
public olmVersion: [number, number, number] | null = null; // populated after initLegacyCrypto
public usingExternalCrypto = false;
private _store!: Store;
public deviceId: string | null;
@@ -1249,7 +1259,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public crypto?: Crypto; // XXX: Intended private, used in code. Being replaced by cryptoBackend
private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
public cryptoCallbacks: CryptoCallbacks; // XXX: Intended private, used in code.
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
public groupCallEventHandler?: GroupCallEventHandler;
public supportsCallTransfer = false; // XXX: Intended private, used in code.
@@ -1269,6 +1279,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected isGuestAccount = false;
protected ongoingScrollbacks: { [roomId: string]: { promise?: Promise<Room>; errorTs?: number } } = {};
protected notifTimelineSet: EventTimelineSet | null = null;
/* @deprecated */
protected cryptoStore?: CryptoStore;
protected verificationMethods?: string[];
protected fallbackICEServerAllowed = false;
@@ -1337,7 +1348,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
this.sessionId = randomString(10);
this.sessionId = secureRandomString(10);
const userId = opts.userId || null;
this.credentials = { userId };
@@ -1598,7 +1609,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Try to rehydrate a device if available. The client must have been
* initialized with a `cryptoCallback.getDehydrationKey` option, and this
* function must be called before initCrypto and startClient are called.
* function must be called before initLegacyCrypto and startClient are called.
*
* @returns Promise which resolves to undefined if a device could not be dehydrated, or
* to the new device ID if the dehydration was successful.
@@ -1825,10 +1836,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns MXID for the logged-in user, or null if not logged in
*/
public getUserId(): string | null {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId;
}
return null;
return this.credentials?.userId ?? null;
}
/**
@@ -1850,7 +1858,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Domain of this MXID
*/
public getDomain(): string | null {
if (this.credentials && this.credentials.userId) {
if (this.credentials?.userId) {
return this.credentials.userId.replace(/^.*?:/, "");
}
return null;
@@ -1861,10 +1869,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns The user ID localpart or null.
*/
public getUserIdLocalpart(): string | null {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId.split(":")[0].substring(1);
}
return null;
return this.credentials?.userId?.split(":")[0].substring(1) ?? null;
}
/**
@@ -2146,7 +2151,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @deprecated libolm is deprecated. Prefer {@link initRustCrypto}.
*/
public async initCrypto(): Promise<void> {
public async initLegacyCrypto(): Promise<void> {
if (!isCryptoAvailable()) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
@@ -2219,7 +2224,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto.
*
* An alternative to {@link initCrypto}.
* An alternative to {@link initLegacyCrypto}.
*
* **WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to
* the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for
* ensuring that only one `MatrixClient` issue is instantiated at a time.
*
* @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
* @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly
@@ -2303,6 +2312,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
CryptoEvent.KeysChanged,
CryptoEvent.DevicesUpdated,
CryptoEvent.WillUpdateDevices,
CryptoEvent.DehydratedDeviceCreated,
CryptoEvent.DehydratedDeviceUploaded,
CryptoEvent.RehydrationStarted,
CryptoEvent.RehydrationProgress,
CryptoEvent.RehydrationCompleted,
CryptoEvent.RehydrationError,
CryptoEvent.DehydrationKeyCached,
CryptoEvent.DehydratedDeviceRotationError,
]);
}
@@ -2316,7 +2333,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Access the crypto API for this client.
*
* If end-to-end encryption has been enabled for this client (via {@link initCrypto} or {@link initRustCrypto}),
* If end-to-end encryption has been enabled for this client (via {@link initLegacyCrypto} or {@link initRustCrypto}),
* returns an object giving access to the crypto API. Otherwise, returns `undefined`.
*/
public getCrypto(): CryptoApi | undefined {
@@ -2425,6 +2442,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @remarks
* Fires {@link CryptoEvent#DeviceVerificationChanged}
*
* @deprecated Not supported for Rust Cryptography.
*/
public setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
const prom = this.setDeviceVerification(userId, deviceId, verified, null, null);
@@ -3063,7 +3082,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}.
*/
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public isSecretStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.secretStorage.isStored(name);
}
@@ -3152,6 +3171,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @returns true if the sender of this event has been verified using
* {@link MatrixClient#setDeviceVerified}.
*
* @deprecated Not supported for Rust Cryptography.
*/
public async isEventSenderVerified(event: MatrixEvent): Promise<boolean> {
const device = await this.getEventSenderDeviceInfo(event);
@@ -3352,7 +3373,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @returns Information object from API, or null if no backup is present on the server.
*
* @deprecated Prefer {@link CryptoApi.getActiveSessionBackupVersion}.
* @deprecated Prefer {@link CryptoApi.getKeyBackupInfo}.
*/
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
let res: IKeyBackupInfo;
@@ -3698,6 +3719,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param opts - Optional params such as callbacks
* @returns Status of restoration with `total` and `imported`
* key counts.
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
*/
public async restoreKeyBackupWithPassword(
password: string,
@@ -3706,6 +3729,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
backupInfo: IKeyBackupInfo,
opts: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
*/
public async restoreKeyBackupWithPassword(
password: string,
targetRoomId: string,
@@ -3713,6 +3739,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
backupInfo: IKeyBackupInfo,
opts: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
*/
public async restoreKeyBackupWithPassword(
password: string,
targetRoomId: string,
@@ -3720,6 +3749,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
backupInfo: IKeyBackupInfo,
opts: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackupWithPassphrase | `CryptoApi.restoreKeyBackupWithPassphrase`}.
*/
public async restoreKeyBackupWithPassword(
password: string,
targetRoomId: string | undefined,
@@ -3743,6 +3775,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param opts - Optional params such as callbacks
* @returns Status of restoration with `total` and `imported`
* key counts.
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public async restoreKeyBackupWithSecretStorage(
backupInfo: IKeyBackupInfo,
@@ -3780,6 +3814,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Status of restoration with `total` and `imported`
* key counts.
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public restoreKeyBackupWithRecoveryKey(
recoveryKey: string,
@@ -3788,6 +3824,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public restoreKeyBackupWithRecoveryKey(
recoveryKey: string,
targetRoomId: string,
@@ -3795,6 +3834,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public restoreKeyBackupWithRecoveryKey(
recoveryKey: string,
targetRoomId: string,
@@ -3802,6 +3844,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public restoreKeyBackupWithRecoveryKey(
recoveryKey: string,
targetRoomId: string | undefined,
@@ -3813,24 +3858,42 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
}
/**
* Restore from an existing key backup via a private key stored locally
* @param targetRoomId
* @param targetSessionId
* @param backupInfo
* @param opts
*
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public async restoreKeyBackupWithCache(
targetRoomId: undefined,
targetSessionId: undefined,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public async restoreKeyBackupWithCache(
targetRoomId: string,
targetSessionId: undefined,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public async restoreKeyBackupWithCache(
targetRoomId: string,
targetSessionId: string,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
/**
* @deprecated Prefer {@link CryptoApi.restoreKeyBackup | `CryptoApi.restoreKeyBackup`}.
*/
public async restoreKeyBackupWithCache(
targetRoomId: string | undefined,
targetSessionId: string | undefined,
@@ -3848,28 +3911,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: undefined,
targetSessionId: undefined,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: string,
targetSessionId: undefined,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: string,
targetSessionId: string,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: string | undefined,
targetSessionId: string | undefined,
backupInfo: IKeyBackupInfo,
@@ -4185,7 +4248,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: an empty object
* @returns Rejects: with an error response.
*/
public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> {
public setAccountData<K extends keyof AccountDataEvents>(
eventType: K,
content: AccountDataEvents[K] | Record<string, never>,
): Promise<{}> {
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!,
$type: eventType,
@@ -4200,7 +4266,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param eventType - The event type
* @returns The contents of the given account data event
*/
public getAccountData(eventType: string): MatrixEvent | undefined {
public getAccountData<K extends keyof AccountDataEvents>(eventType: K): MatrixEvent | undefined {
return this.store.getAccountData(eventType);
}
@@ -4212,7 +4278,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: The contents of the given account data event.
* @returns Rejects: with an error response.
*/
public async getAccountDataFromServer<T extends { [k: string]: any }>(eventType: string): Promise<T | null> {
public async getAccountDataFromServer<K extends keyof AccountDataEvents>(
eventType: K,
): Promise<AccountDataEvents[K] | null> {
if (this.isInitialSyncComplete()) {
const event = this.store.getAccountData(eventType);
if (!event) {
@@ -4220,7 +4288,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
// The network version below returns just the content, so this branch
// does the same to match.
return event.getContent<T>();
return event.getContent<AccountDataEvents[K]>();
}
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!,
@@ -4236,7 +4304,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
public async deleteAccountData(eventType: string): Promise<void> {
public async deleteAccountData(eventType: keyof AccountDataEvents): Promise<void> {
const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
// if deletion is not supported overwrite with empty content
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
@@ -4259,8 +4327,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns The array of users that are ignored (empty if none)
*/
public getIgnoredUsers(): string[] {
const event = this.getAccountData("m.ignored_user_list");
if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
const event = this.getAccountData(EventType.IgnoredUserList);
if (!event?.getContent()["ignored_users"]) return [];
return Object.keys(event.getContent()["ignored_users"]);
}
@@ -4275,7 +4343,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
userIds.forEach((u) => {
content.ignored_users[u] = {};
});
return this.setAccountData("m.ignored_user_list", content);
return this.setAccountData(EventType.IgnoredUserList, content);
}
/**
@@ -4423,11 +4491,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* @param roomId - The room to update the topic in.
* @param topic - The plaintext topic. May be empty to remove the topic.
* @param htmlTopic - Optional.
* @returns Promise which resolves: TODO
* @returns Rejects: with an error response.
*/
public setRoomTopic(roomId: string, topic: string, htmlTopic?: string): Promise<ISendEventResponse> {
public setRoomTopic(roomId: string, topic?: string, htmlTopic?: string): Promise<ISendEventResponse> {
const content = ContentHelpers.makeTopicContent(topic, htmlTopic);
return this.sendStateEvent(roomId, EventType.RoomTopic, content);
}
@@ -5014,7 +5084,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Unsupported) {
throw new Error(
"Server does not support relation based redactions " +
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`,
`roomId ${roomId} eventId ${eventId} txnId: ${txnId as string} threadId ${threadId}`,
);
}
@@ -6087,7 +6157,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.partitionThreadedEvents(matrixEvents);
this.processAggregatedTimelineEvents(room, timelineEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
room.addEventsToTimeline(timelineEvents, true, true, room.getLiveTimeline());
this.processThreadEvents(room, threadedEvents, true);
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
@@ -6199,7 +6269,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
timelineSet.addEventsToTimeline(timelineEvents, true, false, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
this.processThreadEvents(timelineSet.room, threadedEvents, true);
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
@@ -6293,10 +6363,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timeline.initialiseState(res.state.map(mapper));
}
timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch);
timelineSet.addEventsToTimeline(events, true, false, timeline, resNewer.next_batch);
if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
}
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
@@ -6350,10 +6420,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timeline = timelineSet.getLiveTimeline();
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
timelineSet.addEventsToTimeline(events, true, timeline, null);
timelineSet.addEventsToTimeline(events, true, false, timeline, null);
if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
}
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(null, Direction.Forward);
@@ -6616,7 +6686,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// No need to partition events for threads here, everything lives
// in the notification timeline set
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
// if we've hit the end of the timeline, we need to stop trying to
@@ -6659,7 +6729,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, matrixEvents);
this.processThreadRoots(room, matrixEvents, backwards);
@@ -6707,12 +6777,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const newToken = res.next_batch;
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, newToken ?? null);
if (!newToken && backwards) {
const originalEvent =
thread.rootEvent ??
mapper(await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id));
timelineSet.addEventsToTimeline([originalEvent], true, eventTimeline, null);
timelineSet.addEventsToTimeline([originalEvent], true, false, eventTimeline, null);
}
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
@@ -6751,7 +6821,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(timelineEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, timelineEvents);
this.processThreadRoots(
room,
@@ -7290,10 +7360,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const sr = SearchResult.fromJson(roomEvents.results![i], mapper);
const room = this.getRoom(sr.context.getEvent().getRoomId());
if (room) {
// Copy over a known event sender if we can
for (const ev of sr.context.getTimeline()) {
const sender = room.getMember(ev.getSender()!);
if (!ev.sender && sender) ev.sender = sender;
ev.setMetadata(room.currentState, false);
}
}
searchResults.results.push(sr);
@@ -7942,7 +8010,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns A new client secret
*/
public generateClientSecret(): string {
return randomString(32);
return secureRandomString(32);
}
/**
@@ -8190,27 +8258,33 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* @returns Promise which resolves to a LoginResponse object
* @returns Rejects: with an error response.
*
* @deprecated This method has unintuitive behaviour: it updates the `MatrixClient` instance with *some* of the
* returned credentials. Instead, call {@link loginRequest} and create a new `MatrixClient` instance using the
* results. See https://github.com/matrix-org/matrix-js-sdk/issues/4502.
*/
public login(loginType: LoginRequest["type"], data: Omit<LoginRequest, "type">): Promise<LoginResponse> {
return this.http
.authedRequest<LoginResponse>(Method.Post, "/login", undefined, {
...data,
type: loginType,
})
.then((response) => {
if (response.access_token && response.user_id) {
this.http.opts.accessToken = response.access_token;
this.credentials = {
userId: response.user_id,
};
}
return response;
});
return this.loginRequest({
...data,
type: loginType,
}).then((response) => {
if (response.access_token && response.user_id) {
this.http.opts.accessToken = response.access_token;
this.credentials = {
userId: response.user_id,
};
}
return response;
});
}
/**
* @returns Promise which resolves to a LoginResponse object
* @returns Rejects: with an error response.
*
* @deprecated This method has unintuitive behaviour: it updates the `MatrixClient` instance with *some* of the
* returned credentials. Instead, call {@link loginRequest} with `data.type: "m.login.password"`, and create a new
* `MatrixClient` instance using the results. See https://github.com/matrix-org/matrix-js-sdk/issues/4502.
*/
public loginWithPassword(user: string, password: string): Promise<LoginResponse> {
return this.login("m.login.password", {
@@ -8255,6 +8329,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param token - Login token previously received from homeserver
* @returns Promise which resolves to a LoginResponse object
* @returns Rejects: with an error response.
*
* @deprecated This method has unintuitive behaviour: it updates the `MatrixClient` instance with *some* of the
* returned credentials. Instead, call {@link loginRequest} with `data.type: "m.login.token"`, and create a new
* `MatrixClient` instance using the results. See https://github.com/matrix-org/matrix-js-sdk/issues/4502.
*/
public loginWithToken(token: string): Promise<LoginResponse> {
return this.login("m.login.token", {
@@ -8262,6 +8340,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
});
}
/**
* Sends a `POST /login` request to the server.
*
* If successful, this will create a new device and access token for the user.
*
* @see {@link MatrixClient.loginFlows} which makes a `GET /login` request.
* @see https://spec.matrix.org/v1.13/client-server-api/#post_matrixclientv3login
*
* @param data - Credentials and other details for the login request.
*/
public async loginRequest(data: LoginRequest): Promise<LoginResponse> {
return await this.http.authedRequest<LoginResponse>(Method.Post, "/login", undefined, data);
}
/**
* Logs out the current session.
* Obviously, further calls that require authorisation should fail after this
@@ -9213,7 +9305,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceId: string,
notificationSettings: LocalNotificationSettings,
): Promise<{}> {
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const;
return this.setAccountData(key, notificationSettings);
}
@@ -9945,8 +10037,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Get, path, params);
}
public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<any> {
// TODO: Types
public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<Terms> {
const url = this.termsUrlForService(serviceType, baseUrl);
return this.http.requestOtherUrl(Method.Get, url);
}
@@ -10066,7 +10157,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
type: EventType.RoomEncryption,
state_key: "",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
algorithm: "m.megolm.v1.aes-sha2",
},
},
],
@@ -10272,6 +10363,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Resolves: A promise of an object containing the OIDC issuer if configured
* @returns Rejects: when the request fails (module:http-api.MatrixError)
* @experimental - part of MSC2965
* @deprecated in favour of getAuthMetadata
*/
public async getAuthIssuer(): Promise<{
issuer: string;
@@ -10280,6 +10372,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
});
}
/**
* Discover and validate delegated auth configuration
* - delegated auth issuer openid-configuration is reachable
* - delegated auth issuer openid-configuration is configured correctly for us
* Fetches /auth_metadata falling back to legacy implementation using /auth_issuer followed by
* https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
* When successful, validated metadata is returned
* @returns validated authentication metadata and optionally signing keys
* @throws when delegated auth config is invalid or unreachable
* @experimental - part of MSC2965
*/
public async getAuthMetadata(): Promise<OidcClientConfig> {
let authMetadata: unknown | undefined;
try {
authMetadata = await this.http.request<unknown>(Method.Get, "/auth_metadata", undefined, undefined, {
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
});
} catch (e) {
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
const { issuer } = await this.getAuthIssuer();
return discoverAndValidateOIDCIssuerWellKnown(issuer);
}
throw e;
}
return validateAuthMetadataAndKeys(authMetadata);
}
}
function getUnstableDelayQueryOpts(delayOpts: SendDelayedEventRequestOpts): QueryDict {
+2 -1
View File
@@ -78,6 +78,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* Get information about the encryption of an event
*
* @param event - event to be checked
* @deprecated Use {@link CryptoApi#getEncryptionInfoForEvent} instead
*/
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
@@ -107,7 +108,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @param backupInfo - The backup information
* @param privKey - The private decryption key.
*/
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor>;
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor>;
/**
* Import a list of room keys restored from backup

Some files were not shown because too many files have changed in this diff Show More