Compare commits

...

355 Commits

Author SHA1 Message Date
RiotRobot 1ae0c2f3ee v34.3.0 2024-08-13 12:05:45 +00:00
RiotRobot de50129a53 v34.3.0-rc.1 2024-08-06 12:26:55 +00:00
Michael Telatynski 5568dfdd41 Move olm to dependencies as its types are needed downstream
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-06 13:25:55 +01:00
RiotRobot 39216d44ed v34.3.0-rc.0 2024-08-06 12:03:46 +00:00
Michael Telatynski 8c3b249567 Re-add olm dependency which is needed for types
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-06 12:00:30 +01:00
Michael Telatynski b8e40ad2a8 Resetting package fields for development 2024-08-06 11:46:18 +01:00
Michael Telatynski 4e2831764d Merge branch 'master' into develop 2024-08-06 11:46:05 +01:00
Michael Telatynski 09780672aa Fix release-gitflow.yml node version 2024-08-06 11:42:35 +01:00
Andrew Ferrazzutti 0fe53876ec Bump matrix-widget-api (#4336) 2024-08-02 12:10:24 +00:00
Michael Telatynski dfec3dc33c Make code tsc es2022 compliant (#4335)
* Remove redundant global.d.ts definitions

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

* Remove roomId overload

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

* Update base.ts

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-01 15:16:59 +00:00
Andrew Ferrazzutti fbdd78b428 Also check for MSC3757 for session state keys (#4334)
Do not prefix the non-legacy session membership state keys with an
underscore for rooms using MSC3757 (in addtion to MSC3779).
2024-08-01 14:57:29 +00:00
Andrew Ferrazzutti e10c362ef0 Support MSC4157: delayed events via Widget API (#4311) 2024-08-01 14:17:52 +00:00
David Baker 89a9a7fa38 Fix hashed ID server lookups with no Olm (#4333)
* Fix hashed ID server lookups with no Olm

It used the hash function from Olm (presumably to work cross-platform)
but subtle crypto is available on node nowadays so we can just use
that.

Refactor existing code that did this out to a common function, add
tests.

* Test the code when crypto is available

* Test case of no crypto available

* Move digest file to src to get it out of the way of the olm / e2e stuff

* Fix import

* Fix error string & doc

* subtle crypto, not webcrypto

* Extract the base64 part

* Fix test

* Move test file too

* Add more doc

* Fix imports
2024-08-01 10:55:23 +00:00
Andrew Ferrazzutti 687d08dc9d Support MSC4140: Delayed events (#4294)
and use them for more reliable MatrixRTC session membership events.

Also implement "parent" delayed events, which were in a previous version
of the MSC and may be reintroduced or be part of a new MSC later.

NOTE: Still missing is support for sending encrypted delayed events.
2024-07-30 12:43:25 +00:00
RiotRobot 7f91db83d0 v34.2.0 2024-07-30 12:37:58 +00:00
Michael Telatynski 0300d6343f Remove flaky test (#4332)
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/4331
2024-07-29 13:42:07 +00:00
Richard van der Hoff dc1cccfecc Handle late-arriving m.room_key.withheld messages (#4310)
* Restructure eventsPendingKey to remove sender key

For withheld notices, we don't necessarily receive the sender key, so we'll
jhave to do without it.

* Re-decrypt events when we receive a withheld notice

* Extend test to cover late-arriving withheld notices

* update unit tests
2024-07-29 12:11:37 +00:00
Hubert Chathi d32f398345 Fix comment for useAuthorizationHeader config. (#4330) 2024-07-28 02:25:34 +00:00
Timo 0f08c00c07 Be specific about what is considered a MSC4143 call member event. (#4328)
* Be specific about what is considered a MSC4143 call member event.

* review

* check for empty event first

* Optimize for new session type events
If its a session type event we do not want to run two "key in" checks. We expect legacy events to be the less comment type going forward.

* awkward but necessary key count optimization
2024-07-25 10:57:27 +00:00
Timo 6b261b98c9 Add index.ts for matrixrtc module (#4314) 2024-07-25 10:00:11 +00:00
renovate[bot] 99f157a0f1 Update all non-major dependencies (#4323)
* Update all non-major dependencies

* Prettier

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

* Fix types for widget API update

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-07-24 16:40:51 +00:00
renovate[bot] f9f6d81346 Lock file maintenance (#4327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 14:42:05 +00:00
renovate[bot] 46604abe7b Update dependency matrix-widget-api to v1.7.0 (#4326)
* Update dependency matrix-widget-api to v1.7.0

* Fix types

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-24 13:26:04 +00:00
Valere 553758e713 Bump rust sdk wasm version to 7.0.0 (#4316) 2024-07-24 11:53:36 +00:00
renovate[bot] 509e64cfc1 Update dependency typescript to v5.5.4 (#4321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 10:32:17 +00:00
renovate[bot] 60c2e9b3ed Update babel monorepo (#4318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:45:56 +00:00
renovate[bot] cfb21fa80a Update typescript-eslint monorepo to v7.16.1 (#4322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:27:51 +00:00
renovate[bot] c3d7f4e730 Update dependency typedoc-plugin-mdn-links to v3.2.5 (#4320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:17:18 +00:00
renovate[bot] aa97beae44 Update dependency @types/node to v18.19.41 (#4319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:10:23 +00:00
RiotRobot 5feab37166 v34.2.0-rc.0 2024-07-23 11:58:56 +00:00
Michael Telatynski 1a02835ab2 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into staging 2024-07-23 12:51:48 +01:00
David Baker 6f63ff1711 Remove the compare function from utils (#4315)
* Remove the compare function from utils

and change the one use of it to just intantiate a collator and use
it.

This was marked as internal module so this shouldn't be a breaking change.
Of course, react-sdk was using it.

Requires: https://github.com/matrix-org/matrix-react-sdk/pull/12782

* Add simple not-a-perf-test test

* recalculate repeatedly

otherwise we aren't testing anything different

* Use fewer members as it was making the test take a bit too long
2024-07-17 14:18:46 +00:00
RiotRobot 4d90fecb6a v34.1.0 2024-07-16 12:20:13 +00:00
Richard van der Hoff 30a26813ec Deprecate CreateSecretStorageOpts.getKeyBackupPassphrase (#4313)
it doesn't work in rust crypto, and we have no plans to fix it
(https://github.com/element-hq/element-web/issues/27455)
2024-07-13 10:19:14 +00:00
renovate[bot] f17a4fedb9 Lock file maintenance (#4199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 16:31:06 +00:00
renovate[bot] 94e393c9a6 Update dependency typedoc to v0.26.4 (#4309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 08:01:38 +00:00
Richard van der Hoff 53201688a6 Element-R: detect "withheld key" UTD errors, and mark them as such (#4302)
Partial fix to element-hq/element-web#27653
2024-07-09 21:42:58 +01:00
renovate[bot] 996663bf64 Update dependency rimraf to v6 (#4307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 17:12:10 +00:00
renovate[bot] d6e4338a37 Update all non-major dependencies (#4305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 16:48:22 +00:00
renovate[bot] b2665f2128 Update typedoc (#4304)
* Update typedoc

* Update README.md

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 15:30:34 +00:00
renovate[bot] af4b6bc126 Update typescript-eslint monorepo to v7.15.0 (#4306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 14:16:29 +00:00
renovate[bot] 565bb0ef7c Update dependency typescript to v5.5.3 (#4303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 14:15:37 +00:00
Michael Telatynski fe0edcd081 Stop testing on Node 21 as it is EOL (#4308)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 14:32:07 +00:00
RiotRobot 6520e0f54f v34.1.0-rc.3 2024-07-09 13:15:09 +00:00
Michael Telatynski ed7b314e6a Promote olm to a real dep given the types refer to it
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 14:13:50 +01:00
RiotRobot 24eff501e4 v34.1.0-rc.2 2024-07-09 12:17:59 +00:00
Michael Telatynski 51544f25a7 Fix bump-downstreams using incompatible Node version
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 13:17:11 +01:00
RiotRobot a0d73dfaca v34.1.0-rc.1 2024-07-09 12:12:53 +00:00
Michael Telatynski 5d2500b7a7 Fix bump-downstreams using incompatible Node version
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 13:11:25 +01:00
RiotRobot eff52b82e8 v34.1.0-rc.0 2024-07-09 12:04:20 +00:00
Michael Telatynski 2868308079 Prettier 2024-07-08 16:46:14 +01:00
RiotRobot a5ef569717 Resetting package fields for development 2024-07-08 12:18:34 +00:00
RiotRobot c06b22ae7c Merge branch 'master' into develop 2024-07-08 12:18:34 +00:00
RiotRobot 7a51798acb v34.0.0 2024-07-08 12:18:02 +00:00
Richard van der Hoff 712ba617de Remove crypto shims (#4292)
* Inline subtlecrypto shim

The presence of this thing just makes code more confusing.

* Remove pre-node-20 webcrypto hack

Until node 20.0, the webcrypto API lived at `crypto.webCrypto`. It's now
available at the same place as in web -- `globalThis.crypto`.

See: https://nodejs.org/docs/latest-v20.x/api/webcrypto.html#web-crypto-api

* oidc auth test: Clean up mocking

THe previous reset code wasn't really resetting the right thing. Let's just
re-init `window.crypto` on each test.

* Remove `crypto` shim

This isn't very useful any more.
2024-07-05 09:42:06 +00:00
Timo 957329b218 Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)
* Fix room state being updated with old (now overwritten) state and emitting for those updates.

* remove timestamp condition

Add configuration for toStartOfTimeline

* fix timeline tests

* only skip event adding if event_id and replaces_state is set.

* fix room tests

* test skipping insertion

* rename back to lastStateEvent

* store if a state is at the start of a timeline in the RoomState class

* make `isStartTimelineState` a `public readonly` and fix condition.
2024-07-05 09:16:59 +00:00
Richard van der Hoff 1733ec7b7f Remove redundant checks on global.Olm (#4301)
These routines don't use Olm, and we shouldn't be checking for it.
2024-07-04 15:49:56 +00:00
RiotRobot 24c589923b v34.0.0-rc.1 2024-07-04 12:49:22 +00:00
Richard van der Hoff 03ed4f5dd7 Bump node.js requirement to 20. (#4293)
According to
https://github.com/matrix-org/matrix-js-sdk?tab=readme-ov-file#supported-platforms,
we *only* supprt the latest LTS release (which is currenly https://github.com/nodejs/release#release-schedule), so this should be safe.
2024-07-04 12:27:31 +00:00
Joel 6e641a28c0 Add ability to choose how many timeline events to sync when peeking (#4300)
* Add ability to choose how many timeline events to sync when peeking.

* Add a test that covers the new method parameter.

* Formatting.

---------

Co-authored-by: Joel <joel.garplind+github@gmail.com>
2024-07-04 12:14:14 +00:00
ElementRobot 1586de44bd [Backport staging] Fix "Unable to restore session" error (#4299)
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Fixes https://github.com/element-hq/element-web/issues/27666
2024-07-03 20:56:40 +01:00
Richard van der Hoff b36682cb99 Bump matrix-rust-sdk to 6.2.1 (#4298)
Fixes https://github.com/element-hq/element-web/issues/27666
2024-07-03 19:31:23 +00:00
David Baker 04ea2a4e5d Merge pull request #4297 from matrix-org/backport-4296-to-staging
[Backport staging] Fix error when sending encrypted messages in large rooms
2024-07-03 15:22:31 +01:00
Richard van der Hoff b71099d0f8 Bump matrix-rust-sdk to 6.2.0 (#4296)
Fixes https://github.com/element-hq/element-web/issues/27658

(cherry picked from commit ccc2fb5663)
2024-07-03 14:04:38 +00:00
Richard van der Hoff ccc2fb5663 Bump matrix-rust-sdk to 6.2.0 (#4296)
Fixes https://github.com/element-hq/element-web/issues/27658
2024-07-03 13:45:38 +00:00
David Langley 0a7f7efd9d Sync labels with element-meta and add local labels yml for others. (#4295) 2024-07-03 13:36:40 +00:00
Richard van der Hoff ae58d0c8eb Rust crypto: Clean up handling of verification events (#4288)
We had both an `onIncomingKeyVerificationRequest` and an
`onKeyVerificationRequest` which did different, but related, things.

Improve the documentation and reduce the duplication.
2024-07-01 11:32:09 +00:00
Richard van der Hoff 20a6704497 Remove redundant TextEncoder shim (#4290)
I believe the only reason we had this was that, before Node v11.0,
`TextEncoder` wasn't available in the global object. Nowadays it is (see
https://nodejs.org/api/util.html#class-utiltextencoder), so let's get rid of
it.
2024-07-01 09:15:01 +00:00
Kegan Dougal 3337bda752 Remove useless log line (#4289)
- it was spammy: https://github.com/element-hq/element-web/issues/27031
- it didn't actually log the duration, because the `block` function didn't
  `await` the inner promise.
2024-06-28 15:49:52 +00:00
Richard van der Hoff d90292bff5 Use prebuilt js-sdk for node example (#4286)
This example seems to have been broken by the switch to Typescript. We can't
just symlink in `../..` because that gives us the typescript version of the
source, which, obviously, doesn't work in node.

Instead, make sure we use a prebuilt version of the js-sdk.

It's actually even more broken as of js-sdk 33.0.0, thanks to the switch to ES
modules (#4187), but we'll get to that later.
2024-06-27 16:58:26 +00:00
Richard van der Hoff 3de0c02757 Remove redundant hack for using the old pickle key in rust crypto (#4282)
* Remove redundant hack for using the old pickle key in rust crypto

* Fix tests
2024-06-27 15:43:54 +00:00
Richard van der Hoff 65b9c31f9b Rename crypto-api.ts -> crypto-api/index.ts (#4283)
I found it quite confusing having `CryptoApi` be defined so far from the
`crypto-api` folder.
2024-06-26 21:02:40 +00:00
Richard van der Hoff d629a685c2 Declare matrix-js-sdk as an ES module (#4285)
* Declare matrix-js-sdk as an ES module

* Rename `babel.config.js` to show it is a CommonJS module

... otherwise it gets broken by `scripts/switch_package_to_release.js`
2024-06-26 17:11:29 +00:00
Timo 0210106be2 Add fetching the well known in embedded mode. (#4259)
* Add fetching the well known in embedded mode.

This is used to load the focus from the well known in elment-call.

* revert what we dont want in this PR.

* Update src/client.ts

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-06-26 09:29:11 +00:00
renovate[bot] 3e05a71068 Update dependency typedoc-plugin-coverage to v3.3.0 (#4281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 22:21:51 +00:00
renovate[bot] c755810d9c Update dependency typedoc-plugin-mdn-links to v3.2.0 (#4279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 17:06:31 +00:00
renovate[bot] 2d492f60a0 Update dependency typescript to v5.5.2 (#4280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 16:49:31 +00:00
renovate[bot] 16db2c5f9a Update dependency @types/node to v18.19.39 (#4278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 16:30:40 +00:00
renovate[bot] 29c02d8c37 Update all non-major dependencies (#4268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 16:30:03 +00:00
Michael Telatynski 0f98df158c Fix ingest of release notes wiping out the parent notes (#4266)
* Fix ingest of release notes wiping out the parent notes

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

* Remove redundant reusable workflow input

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-25 16:24:13 +00:00
renovate[bot] 2ea4ce0bb6 Update dependency bs58 to v6 (#4274)
* Update dependency bs58 to v6

* Update import

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

* 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-06-25 16:02:43 +00:00
renovate[bot] 3e0017fecf Update actions/checkout digest to 692973e (#4267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 15:25:04 +00:00
renovate[bot] a0073ddaaf Update typescript-eslint monorepo to v7.14.1 (#4272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 14:36:52 +00:00
renovate[bot] c29e116c0c Update dependency typescript to v5.5.2 (#4271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 14:36:11 +00:00
renovate[bot] d9f372ca79 Update dependency eslint-plugin-unicorn to v54 (#4275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 13:54:38 +00:00
renovate[bot] 6417f4fac7 Update dependency fetch-mock to v10 (#4276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-25 16:02:45 +01:00
renovate[bot] 4bae83f59f Update dependency @types/uuid to v10 (#4273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 13:53:11 +00:00
renovate[bot] b8c68eb102 Update dependency @types/node to v18.19.39 (#4269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 13:50:33 +00:00
renovate[bot] 8790cde6d4 Update dependency typedoc to ^0.26.0 (#4270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-25 15:21:27 +01:00
RiotRobot ab6260074d v34.0.0-rc.0 2024-06-25 12:50:01 +00:00
Andrew Ferrazzutti 25a7c9e140 Prefix the user+device state key if needed (#4262)
* Prefix the user+device state key if needed

No need to prefix it for rooms that use MSC3779.
Otherwise, prefix it to bypass the auth rule for state events with keys
starting with @.

* Use RegExp.exec() method instead

Sonar typescript:S6594

* Split nested ternary operator into method

Sonar typescript:S3358

* Add test coverage
2024-06-21 17:31:42 +00:00
Michael Telatynski 78b6b878bd Remove domexception polyfill, has been native in NodeJS since v17 (#4253)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-21 14:15:54 +00:00
Richard van der Hoff 4ccb72c0f2 Element-R: Fix resource leaks in verification logic (#4263)
* Move `RustVerificationRequest.onChange` out to a method

The only reason it was an inner function in the first place was to avoid
storing a reference in the class to `outgoingRequestProcessor`. That changed
with d1dec4cd08.

* Fix reference cycles in rust verification code
2024-06-21 12:55:43 +00:00
Richard van der Hoff 9f1aebbdcb Bump ES target version to ES2022 (#4264)
* Bump ES target version to ES2022

I want to be able to use `WeakRef`, and per
https://github.com/element-hq/element-web/issues/24913#issuecomment-2182448007,
I believe this should be safe.

* room.ts: Fix initialisation order

It seems that ES2022 causes typescript to change the initialization order of
regular properties vs parameter properties
(https://github.com/microsoft/TypeScript/issues/45995), so we need to rearrange
the initializations to avoid an error.

In practice, it might be fine because we have enabled
`babel-plugin-transform-class-properties`, which moves the initialization back
after the parameter property, but we shoudn't rely on that, and anyway it
upsets the linter.
2024-06-21 11:50:28 +00:00
Andrew Ferrazzutti 6a15e8f1a0 Use legacy call membership if anyone else is (#4260)
* Use legacy call membership if anyone else is

* Convert nullish to boolean

* Update tests

* Lint

* Use computed decision to use legacy events or not

* Check if discovered legacy sessions are ongoing

* Lint

* Lint again

* Increase test coverage
2024-06-21 11:40:27 +00:00
Richard van der Hoff 238eea0ef5 Upgrade Rust Crypto SDK to 6.1.0 (#4261)
Fixes https://github.com/element-hq/element-web/issues/27590
2024-06-20 08:28:17 +00:00
Michael Telatynski ab6f86536f Replace deprecated babel proposal plugins (#4254)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-19 15:11:38 +00:00
David Baker 819fc75202 Fetch capabilities in the background (#4246)
* Fetch capabilities in the background

& keep them up to date

* Add missed await

* Replace some more runAllTimers

and round down the wait time for sanity

* Remove double comment

* Typo

* Add a method back that will fetch capabilities if they're not already there

* Add tests

* Catch exception here too

* Add test for room version code
2024-06-19 10:24:56 +00:00
renovate[bot] c70aa33367 Update typescript-eslint monorepo to v7.13.1 (#4257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 09:50:23 +00:00
renovate[bot] 240b43b652 Update typedoc (#4258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 08:57:01 +00:00
renovate[bot] 697d5d31d1 Update all non-major dependencies (#4256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 08:54:29 +00:00
Robin b1701ff571 Correctly transform base64 with multiple instances of + or / (#4252)
String.replace only replaces a single instance of the search pattern by default; we need a regex in g mode if we want to replace them all.
2024-06-18 14:58:16 +00:00
Michael Telatynski c55289ec65 Use server name instead of homeserver url to allow well-known lookups during QR OIDC reciprocation (#4233)
* Use server name instead of homeserver url to allow well-known lookups during QR OIDC reciprocation

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>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-18 13:03:14 +00:00
RiotRobot 987ec1e62f Resetting package fields for development 2024-06-18 11:55:23 +00:00
RiotRobot 76b240cf57 Merge branch 'master' into develop 2024-06-18 11:55:21 +00:00
RiotRobot a4c4e7e275 v33.1.0 2024-06-18 11:54:47 +00:00
Johannes Marbach 3f5a994a24 Add via parameter for MSC4156 (#4247)
* Add via parameter for MSC4156

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

* Always include both parameters

* Fix tests

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-06-18 08:14:48 +00:00
Timo d754392410 Make the js-sdk compatible with MSC preferred foci and active focus. (#4195)
* Refactor to preferred and active foci.

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

* make the sdk compatible with MSC4143 but still be backwards compatible

* comment fixes

* also fallback to legacy if the current member event is legacy

* use XOR types

* use EitherAnd

* make livekit Foucs types simpler

* review

* fix tests

* test work

* more review + more tests

* remove unnecassary await that is in conflict with the comment

* make joinRoomSession sync

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Andrew Ferrazzutti <af_0_af@hotmail.com>

* review

* fix

* test

* review

* review

* comment clarification

* typo

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Andrew Ferrazzutti <af_0_af@hotmail.com>
2024-06-17 13:02:29 +00:00
Michael Telatynski 7ecaa53e34 Work around spec bug for m.room.avatar state event content type (#4245)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-17 09:30:13 +00:00
RiotRobot 222e95d33f v33.1.0-rc.1 2024-06-14 12:18:37 +00:00
ElementRobot 2ee43cade7 [Backport staging] Fix screen sharing in recent Chrome (#4243)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-06-13 20:54:53 +01:00
David Baker 9218f6380c Fix screen sharing in recent Chrome (#4241)
* Fix screen sharing in recent Chrome

Dreadful hack to work around a bug in recent chrome/electron's
WebRTC, as explained.

I'm not sure which is the least hideous out of this (ie. repeatedly
calling setCodecPreferences and seeing if it crashes each time) or
hardcoding the bad codec and skipping it. Opinions welcome.

* Unused import

* Remove commented line
2024-06-13 18:44:30 +00:00
Travis Ralston 661ba76763 Use stable endpoints for MSC3916 (#4239)
* Use stable endpoints for MSC3916

* appease the linter
2024-06-13 17:03:25 +00:00
Michael Telatynski 4cb851c51a Replace usages of setImmediate with setTimeout for wider compatibility (#4240)
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-06-13 16:01:33 +01:00
renovate[bot] 5a3d24abc2 Update typescript-eslint monorepo to v7.12.0 (#4237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 13:15:29 +00:00
renovate[bot] 3eed74f1a6 Update dependency uuid to v10 (#4238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 12:26:43 +00:00
renovate[bot] 10e7a2d997 Update dependency typedoc-plugin-mdn-links to v3.1.28 (#4236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 12:17:02 +00:00
renovate[bot] 969ecdb6fb Update dependency @types/node to v18.19.34 (#4235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 12:16:52 +00:00
renovate[bot] e8b91f2729 Update babel monorepo to v7.24.7 (#4234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 12:16:38 +00:00
renovate[bot] f80366ff30 Update all non-major dependencies (#4229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 12:09:01 +00:00
RiotRobot 395c3cfcd6 v33.1.0-rc.0 2024-06-11 12:06:39 +00:00
Hubert Chathi f95954c233 Add support for stable name for MSC4115 (#4232)
* add support for stable name for MSC4115

* fix types issues

* prettier

* actually, it still returns `undefined`
2024-06-07 08:47:17 +00:00
Michael Telatynski fa5f2d389a Fix incorrect assumptions about required fields in /search response (#4228)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-06 21:01:16 +00:00
Michael Telatynski 9fc557fc6b Fix typo
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-06 12:11:16 +01:00
Michael Telatynski 6436fbb99f MSC4108 support OIDC QR code login (#4134)
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2024-06-06 09:57:26 +01:00
Michael Telatynski 87c2ac3ffa Use LegacyRendezvousFailureReason over RendezvousFailureReason (#4231)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-05 15:56:37 +00:00
Richard van der Hoff 43022d5b2f RustCrypto: fix ordering of methods (#4230)
* RustCrypto: Move CryptoBackend impl to CryptoBackend impl section

Given there is a `CryptoBackend implementation` section, the methods
implementing CryptoBackend should be there.

* RustCrypto: Fix documentation on dehydration methods

* RustCrypto: reunite `resetKeyBackup` with its helper

A couple of new methods had snuck into the middle.
2024-06-05 12:02:45 +00:00
renovate[bot] a0fadeb4ec Update babel monorepo to v7.24.6 (#4221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-05 10:14:04 +00:00
Michael Telatynski a3cea8ce7d Add crypto methods for export and import of secrets bundle (#4227)
* Add crypto methods for OIDC QR code login

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

* Improve test

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

* Revert test due to hang inside Rust.

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

* Iterate

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

* Update test name

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

* Update test name

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-05 09:27:20 +00:00
renovate[bot] c88487da07 Update all non-major dependencies (#4226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-04 17:21:20 +00:00
RiotRobot 89875b8e31 Resetting package fields for development 2024-06-04 13:18:49 +00:00
RiotRobot 9c94393d76 Merge branch 'master' into develop 2024-06-04 13:18:48 +00:00
RiotRobot 7850294a4b v33.0.0 2024-06-04 13:18:11 +00:00
renovate[bot] 131e81401a Update typescript-eslint monorepo to v7.10.0 (#4223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-30 16:21:12 +00:00
David Baker 5c27e30302 Fix the queueToDevice tests for the new fakeindexeddb (#4225)
https://github.com/dumbmatter/fakeIndexedDB/pull/93 causes a bunch
of tests to start failing because the fake timers need running in
order for fake indexeddb to work. It also seems to cause failures
to bleed between tests somehow if fake timers are enabled/disabled.
This keeps all the fake timer tests in one suite and all the others
in another, which appears to work.

This should allow https://github.com/matrix-org/matrix-js-sdk/pull/4224
to be merged.
2024-05-30 15:16:42 +00:00
renovate[bot] 8dfb6de3cc Update all non-major dependencies (#4220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-30 14:10:12 +00:00
renovate[bot] 042610310f Update dependency typedoc-plugin-mdn-links to v3.1.27 (#4222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-30 13:42:20 +00:00
renovate[bot] 8535604200 Update actions/checkout digest to a5ac7e5 (#4219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-30 11:45:14 +00:00
Hugh Nimmo-Smith 3ee64722c5 Add note about MSC3886, MSC3903 and MSC3906 being closed (#4189)
* Add note about MSC3886, MSC3903 and MSC3906 being closed

* Move comments in to jsdoc

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-30 11:23:24 +00:00
RiotRobot 9d6210b3f9 v33.0.0-rc.0 2024-05-29 12:54:25 +00:00
Andy Balaam 909caab74e Don't run migration for Rust crypto if the legacy store is empty (#4218)
* Don't run migration for Rust crypto if the legacy store is empty

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

* Add copyright for the TypeScript files in legacy DB dumps

* Provide a type for the accountPickle we check for before migration

* Remove redundant backup response

This is unused

* Simplify keys response

* Downgrade log message.

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
2024-05-28 17:43:25 +00:00
Michael Telatynski 7c87625157 Remove more deprecated methods, fields, and exports (#4217) 2024-05-28 09:12:55 +01:00
Richard van der Hoff b19817bb73 Bump matrix-sdk-crypto-wasm to 5.0.0 (#4216)
Slightly more involved than normal because it requires us to pass a backup version into OlmMachine.importBackedUpRoomKeys.

On the other hand we can now re-enable the test that was disabled in #4214 due to matrix-org/matrix-rust-sdk#3447

Fixes: element-hq/element-web#27165
2024-05-24 12:10:52 +01:00
Richard van der Hoff 36196ea422 initRustCrypto: allow app to pass in the store key directly (#4210)
* `initRustCrypto`: allow app to pass in the store key directly

... instead of using the pickleKey. This allows us to avoid a slow PBKDF
operation.

* Fix link in doc-comment
2024-05-24 09:52:34 +00:00
renovate[bot] a81adf542e Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.10.0 (#4214)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.10.0

* Disable affected test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-23 11:19:41 +00:00
RiotRobot a49bc3ddf4 Merge branch 'master' into develop 2024-05-22 12:04:33 +00:00
RiotRobot a86d4ceb49 v32.4.0 2024-05-22 12:04:07 +00:00
R Midhun Suresh 8c3be2a56a Update organization (#4212) 2024-05-21 15:55:49 +00:00
RiotRobot 38898a60c7 v32.3.0 2024-05-21 12:24:52 +00:00
Bayyr Oorjak fd3a4d4403 Preserve ESM for async imports to work correctly (#4187)
* fix: fix lazy rust crypto import

* test: use "commonjs" for tests because of circular deps

* chore: revert commonjs for "module"

* refactor: remove unnecessary example

* refactor: add comments

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>

* refactor: improve comment

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>

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

* Update babel.config.js

---------

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-05-21 11:48:00 +00:00
RiotRobot 93d96281fd Resetting package fields for development 2024-05-21 12:25:26 +00:00
RiotRobot 944dc51c58 Merge branch 'master' into develop 2024-05-21 12:25:25 +00:00
renovate[bot] c6b43dd176 Update dependency eslint-plugin-unicorn to v53 (#4209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 16:56:22 +00:00
Michael Telatynski f03dd7b7bc Remove deprecated methods and fields (#4201)
* Remove legacy `threepidCreds` field

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

* Remove deprecated shouldUpgradeToVersion

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

* Remove `added` legacy login request field

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

* Remove deprecated re-exports

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

* Remove `home_server` field

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

* Update imports in tests

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 15:23:04 +00:00
Michael Telatynski 51fa1866a9 Wire up verification cancel & mismatch for rust crypto (#4202)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 14:58:24 +00:00
renovate[bot] d76fb2baa0 Update all non-major dependencies (#4207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 11:51:51 +00:00
renovate[bot] 3feafc9c17 Update dependency typedoc-plugin-mdn-links to v3.1.25 (#4206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 11:28:29 +00:00
Michael Telatynski c9075b3dba Only pass id_server if we had one to begin with (#4200)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 10:59:42 +00:00
renovate[bot] 69c474dda7 Update dependency @types/node to v18.19.33 (#4205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:54:21 +00:00
renovate[bot] 73ce51065f Update mheap/github-action-required-labels digest to 5847eef (#4204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:26:32 +00:00
renovate[bot] 5d0407d0a6 Update actions/checkout digest to 0ad4b8f (#4203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:23:58 +00:00
RiotRobot 3a4b02d8e6 v32.3.0-rc.0 2024-05-15 09:04:25 +00:00
Kegan Dougal d421e7f829 Run complement-crypto in CI (#4197)
* Maybe run complement-crypto

* Use existing checkout

* Test that things fail if crypto breaks

* Fix test; run only on merge queue

* Prettier

* Maybe get it working in a merge queue
2024-05-13 10:26:27 +00:00
Michael Telatynski 9fd051af33 Update downstream-end-to-end-tests.yml 2024-05-13 11:20:47 +01:00
Michael Telatynski b78a1ad889 Hotfix types export point
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-10 13:26:07 +01:00
Michael Telatynski a25cdcecaa Fix state_events.ts types (#4196)
* Fix state_events.ts types

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>
2024-05-10 10:55:44 +00:00
RiotRobot 2a716bd076 Resetting package fields for development 2024-05-07 12:16:24 +00:00
RiotRobot ef1db8d664 Merge branch 'master' into develop 2024-05-07 12:16:23 +00:00
RiotRobot 22865fd834 v32.2.0 2024-05-07 12:15:48 +00:00
Michael Telatynski c4fe564855 Simplify OIDC types & export decodeIdToken (#4193)
* Fix types

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

* Export `decodeIdToken`

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-07 10:21:57 +00:00
Michael Telatynski 9ecb1a0381 Fix sendEventHttpRequest for m.room.redaction events without redacts (#4192)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-07 08:50:37 +00:00
Travis Ralston ef9490c7b1 Add missing (and common-ish) HTTP method verbs to types (#4188) 2024-05-02 23:09:15 +00:00
Travis Ralston 402adfbe8a Add helpers for authenticated media, and associated documentation (#4185)
* Add helpers for authenticated media, and associated documentation

* Appease the linter
2024-05-02 21:11:09 +00:00
renovate[bot] 41e8c2af34 Update dependency typedoc-plugin-coverage to v3.1.1 (#4186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-01 10:23:17 +00:00
renovate[bot] 4843b40296 Update dependency typedoc-plugin-mdn-links to v3.1.22 (#4182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:18:45 +00:00
renovate[bot] bc2c870152 Update babel monorepo to v7.24.5 (#4181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:17:37 +00:00
renovate[bot] 7c7b2817d3 Update all non-major dependencies (#4184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:16:31 +00:00
renovate[bot] 9f78202ecd Update typescript-eslint monorepo to v7.7.1 (#4183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-30 14:40:57 +01:00
RiotRobot abc9911e95 v32.2.0-rc.0 2024-04-30 12:02:20 +00:00
David Baker efdae0d66f Remove spammy RTCSession log line (#4180)
This can get quite spammy in rooms with legacy calls and isn't really
neccesary: remove it to cut down on the log spam.
2024-04-30 10:43:20 +00:00
David Baker 2bf554761c Add some comments to the release drafter workflows (#4140)
* Add some comments to the release drafter workflows

* Rename component workflow so they have different names

* Fix comment

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-30 09:38:07 +00:00
Michael Telatynski c2687643b5 Update downstream-end-to-end-tests.yml 2024-04-30 10:30:19 +01:00
Michael Telatynski a33758eda6 Update downstream-end-to-end-tests.yml 2024-04-30 10:28:44 +01:00
Michael Telatynski 8faed02cc5 Update sonarcloud.yml 2024-04-29 17:22:32 +01:00
Michael Telatynski 3ae0dab47a Update sonarcloud.yml 2024-04-29 16:59:55 +01:00
Michael Telatynski 95394e4cbe Update sonarcloud.yml 2024-04-29 16:52:54 +01:00
Michael Telatynski 8c9bbc01fc Update downstream-end-to-end-tests.yml 2024-04-29 16:20:24 +01:00
Michael Telatynski eb888791a3 Pass all args to Sonar via sonar-project.properties file (#4179)
* Pass all args to Sonar via sonar-project.properties file

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

* Update Sonarcloud action to v3.1

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

* Apply suggestions from code review

Co-authored-by: davidegirardi <16451191+davidegirardi@users.noreply.github.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: davidegirardi <16451191+davidegirardi@users.noreply.github.com>
2024-04-29 15:12:00 +00:00
Michael Telatynski 6c0b2f55e1 Delete .github/workflows/downstream-artifacts.yml 2024-04-29 10:53:12 +01:00
Michael Telatynski c9a5eaece3 Simplify Playwright CI (#4178) 2024-04-29 08:49:00 +01:00
Hubert Chathi 64505de36b Use a different error code for UTDs when user was not in the room (#4172)
* use a different error code for UTDs when user was not in the room

* if user is invited, treat it as unexpected UTD
2024-04-26 13:38:10 +00:00
Valere 65d858f9a3 Fix rust migration when ssss secret not encrypted (#4168) 2024-04-26 12:59:17 +00:00
RiotRobot 1da5e8f56a Resetting package fields for development 2024-04-23 12:34:26 +00:00
RiotRobot 5efd4c2915 Merge branch 'master' into develop 2024-04-23 12:34:25 +00:00
RiotRobot bc03950f8a v32.1.0 2024-04-23 12:33:56 +00:00
Travis Ralston c09da9a23f Modernize window.crypto access constants (#4169)
* Force service worker-safe crypto when operating under a service worker

* Fix tests/unsafe execution

* Further fix tests?

* Docs would probably be good

* Define a type guard function

https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards

* Use `@types` repo

* Maybe don't modify tsconfig, I guess

* Revert "Use `@types` repo"

This reverts commit db46bcf1db4b94fbc7e0c97a20d5d800fcb2768b.

* Use a different type for Window

* Simplify the crypto accessors
2024-04-22 19:44:01 +00:00
Andrew Ferrazzutti e874468ba3 Improve compliance with MSC3266 (#4155)
* Fix fields of MSC 3266 summary object

Also remove redundant room_type field which is inherited from elsewhere

* Export the MSC 3266 summary type

* Use proper endpoint for MSC 3266 summary lookup

Use the endpoint recommended by the MSC

* Rename newly-exported symbol to not start with I

* Use "export type"

* Lint

* Fix type of "encryption" field

* Add TSDoc documentation

* Add basic integration test for getRoomSummary

* Lint

* Use fallback endpoint for MSC3266

* Improve test coverage

* Lint

* Refactor async catch to satisfy linter

* Increase test coverage
2024-04-22 17:39:10 +00:00
Johannes Marbach 6fedda91f9 Use encoded URI components when storing sessions in memory crypto store (#4170)
* Use encoded URI components when storing sessions in memory crypto store

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

* Add URI en-/decoding to missing methods

* Extract convenience functions

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-04-22 08:12:54 +00:00
Valere d22a39f5d7 Element R: fix isCrossSigningReady not checking identity trust (#4156)
* Fix inconsistency between rust and legacy

* Add tests

* Review: better comment

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

* review: Better doc

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

* Simplify test data and some comments

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-04-17 13:36:52 +00:00
Timo 4fc6ba884e add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk (#4152)
Signed-off-by: Timo K <toger5@hotmail.de>
2024-04-17 10:28:40 +00:00
Richard van der Hoff c30e498013 Crypto: use a new error code for UTDs from device-relative historical events (#4139)
* Add `PerSessionKeyBackupDownloader.isKeyBackupDownloadConfigured()`

* Add new `RustBackupManager.getServerBackupInfo`

... and a convenience method in PerSessionKeyBackupDownloader to access it.

* Crypto.spec: move `useRealTimers` to global `afterEach`

... so that we don't need to remember to do it everywhere.

* Use fake timers for UTD error code tests

This doesn't have any effect on the tests, but *does* stop jest from hanging
when you run the tests in in-band mode. It shouldn't *really* be needed, but
using fake timers gives more reproducible tests, and I don't have the
time/patience to debug why it is needed.

* Use new error codes for UTDs from historical events
2024-04-17 10:26:41 +00:00
renovate[bot] 8240bf0ae7 Update shogo82148/actions-upload-release-asset digest to 8f032ef (#4157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 15:59:04 +00:00
renovate[bot] 2321c44687 Update typedoc (#4162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 15:22:17 +00:00
renovate[bot] 0137e9d5a8 Update dependency eslint-plugin-unicorn to v52 (#4166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 14:29:48 +00:00
renovate[bot] 28bbc51752 Update dependency @types/node to v18.19.31 (#4160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 14:06:49 +00:00
renovate[bot] 0db3ac9b43 Update dependency eslint-plugin-jest to v28 (#4165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:56:50 +00:00
renovate[bot] 53039b78ee Update all non-major dependencies (#4158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:25:06 +00:00
renovate[bot] 2a06d19431 Update babel monorepo to v7.24.4 (#4159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:24:46 +00:00
renovate[bot] a747eef04c Update dependency typescript to v5.4.5 (#4161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:24:27 +00:00
renovate[bot] 583823c2ef Update dependency knip to v5 (#4167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:21:14 +00:00
renovate[bot] 26d13c15c3 Update typescript-eslint monorepo to v7.6.0 (#4163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:20:45 +00:00
RiotRobot c850ca3179 v32.1.0-rc.0 2024-04-16 12:16:13 +00:00
Valere 8438533532 Validate backup private key before migrating it (#4114)
* Migrate own identity trust to rust crypto

* Fix gendoc not happy if msk of IDownloadKeyResult has a signature

* add missing mock

* code review

* Code review

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* review move function down in file

* Review gh suggestion

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

* Review gh suggestion

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

* Review: Cleaning tests, renaming

* Review: better comment

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

* Comment paragraphs

* retry until initial  key query is successfull

* Validate backup private key before migrating it

* post merge fix

* Fix test, missing mock

* Use crypto wasm instead of lib olm to check backup key

* typo

* code review

* quick lint

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-04-15 07:17:14 +00:00
David Baker 475f82c5ce Update git-get-release action (#4148)
which appears to be breaking renovate somehow, and could probably use
and update anyway.
2024-04-11 09:36:07 +00:00
Hubert Chathi 936e7c3072 Add support for device dehydration v2 (Element R) (#4062)
* initial implementation of device dehydration

* add dehydrated flag for devices

* add missing dehydration.ts file, add test, add function to schedule dehydration

* add more dehydration utility functions

* stop scheduled dehydration when crypto stops

* bump matrix-crypto-sdk-wasm version, and fix tests

* adding dehydratedDevices member to mock OlmDevice isn't necessary any more

* fix yarn lock file

* more tests

* fix test

* more tests

* fix typo

* fix logic for checking if dehydration supported

* make changes from review

* add missing file

* move setup into another function

* apply changes from review

* implement simpler API

* fix type and move the code to the right spot

* apply suggestions from review

* make sure that cross-signing and secret storage are set up
2024-04-11 04:01:47 +00:00
RiotRobot 82ed7bd86a Resetting package fields for development 2024-04-09 10:07:09 +00:00
RiotRobot cb67eae858 Merge branch 'master' into develop 2024-04-09 10:07:07 +00:00
RiotRobot e4937e6222 v32.0.0 2024-04-09 10:06:41 +00:00
Michael Telatynski 5cdd524da7 OIDC improvements in prep of OIDC-QR reciprocation (#4149)
* Add `device_authorization_endpoint` field to OIDC issuer well-known metadata

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

* Allow `validateIdToken` to skip handling nonce when none is present

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

* Tweak registerOidcClient to check OIDC grant_types_supported before registration

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-08 14:07:00 +00:00
Michael Telatynski 0ff0093380 Add knip unused code & dependency analyser (#4013) 2024-04-08 11:04:40 +01:00
Valere b352405c89 ElementR| Retry query backup until it works during migration to avoid spurious correption error popup (#4113)
* retry query backup until it works during migration

* Add log line when fails to get backup during migration
2024-04-08 07:11:06 +00:00
renovate[bot] 0d73d0c6c7 Update typescript-eslint monorepo to v7.4.0 (#4146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 09:49:35 +00:00
renovate[bot] d2f76d4956 Update dependency typescript to v5.4.3 (#4145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 09:23:09 +00:00
renovate[bot] c680dd7eb2 Update dependency @types/node to v18.19.28 (#4144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:37:32 +00:00
renovate[bot] e24bb0f50c Update babel monorepo to v7.24.3 (#4143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:36:51 +00:00
renovate[bot] 1ed3b13f0d Update all non-major dependencies (#4142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 17:58:13 +00:00
renovate[bot] 4f628bf64c Update mheap/github-action-required-labels digest to 132879b (#4141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 17:08:08 +00:00
RiotRobot 7d5c003716 v32.0.0-rc.0 2024-04-02 16:15:04 +00:00
Richard van der Hoff dbab185f9d Refactoring and simplification in decryption error handling (#4138)
* Clean up decryption failure integ tests

* Fix the names
* Stop waiting as soon as the event is decrypted, even if code is wrong (so
  tests fail rather than time out if the code is wrong)

* Bump timeouts on some tests

These tend to fail due to slow init of wasm artifacts

* Factor out `onDecryptionKeyMissingError` call

* Factor out `onMegolmDecryptionError`
2024-04-02 13:39:49 +00:00
Richard van der Hoff cfcd191cbf Update matrix-rust-sdk-crypto-wasm to 4.9.0 (#4137) 2024-04-02 13:09:59 +00:00
RiotRobot 514633c5fa Merge branch 'master' into develop 2024-03-28 16:45:10 +00:00
RiotRobot 5bffb7df4f v31.6.1 2024-03-28 16:44:30 +00:00
David Baker 9e1897dcd0 Merge pull request #4136 from matrix-org/backport-4135-to-staging
Fix merging of default push rules (backport)
2024-03-28 16:35:36 +00:00
David Baker 5f3ddc37a1 Merge pull request #4135 from matrix-org/t3chguy/fix/27173
Fix merging of default push rules

(cherry picked from commit 78a225795b)
2024-03-28 16:28:33 +00:00
David Baker 78a225795b Merge pull request #4135 from matrix-org/t3chguy/fix/27173
Fix merging of default push rules
2024-03-28 16:23:58 +00:00
Michael Telatynski 467b49a0dc Add test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 15:52:12 +00:00
Michael Telatynski 06e083874a Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 14:59:52 +00:00
Michael Telatynski 0f25429849 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 13:41:31 +00:00
Michael Telatynski 32ddf2813d Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 12:55:44 +00:00
Michael Telatynski 1ed082f3d4 Fix merging of default push rules
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 11:58:52 +00:00
RiotRobot 706002cdcb Resetting package fields for development 2024-03-26 16:26:09 +00:00
RiotRobot 731de1108c Merge branch 'master' into develop 2024-03-26 16:26:07 +00:00
RiotRobot 2da6c0c605 v31.6.0 2024-03-26 16:25:15 +00:00
Richard van der Hoff 9f1d0c3896 Add new decryptExistingEvent test helper (#4133)
* grammar fix

* IEncryptionResult -> EncryptionResult

These are the same thing; the former is the old name.

* Support setting event IDs

* Helper for decrypting existing decryption failures
2024-03-25 14:10:58 +00:00
Gabri 0b290fffa1 Improve types for IPowerLevelsContent and hasSufficientPowerLevelFor (#4128)
Signed-off-by: Gabriele Messina <56839513+galash13@users.noreply.github.com>
2024-03-25 13:58:38 +00:00
Michael Telatynski 97844f0e47 Improve types for sendEvent (#4108) 2024-03-25 12:48:49 +00:00
Michael Telatynski 85a55c79cd Remove various deprecated methods & re-exports (#4125) 2024-03-25 12:21:11 +00:00
Johannes Marbach 63d4195453 Use RoomEvent.MyMembership in auto-join example (#4130)
This seems easier / more efficient than listening to all membership events and then filtering by user ID.
2024-03-25 09:10:42 +00:00
Richard van der Hoff d5a35f8a99 Add new enum for verification methods. (#4129)
* Define constants for the verification methods.

* Remove some confusing references to the *old* `VerificationMethod`
2024-03-22 17:17:31 +00:00
Richard van der Hoff d1259b241c Clean up code for handling decryption failures (#4126)
Various improvements, including:

* Defining an enum for decryption failure reasons
* Exposing the reason code as a property on Event
2024-03-22 17:15:27 +00:00
David Langley a573727662 Remove the logic that throws when the lazy loading options has changed. (#4124)
* remove InvalidStoreState and the logic that checks for the change in the lazyLoading client option

* lint
2024-03-22 16:36:23 +00:00
Richard van der Hoff dce8acbf17 Add some test utils in a new entrypoint (#4127)
* Clean up README a little

This just removes some of the most egregious lies and outdated stuff. There's a
*lot* more that can be done here.

* Add some test utils in a new entrypoint

* Fix comment

* Update src/testing.ts
2024-03-22 14:10:55 +00:00
David Baker 4ba1341f8f Fix highlights from threads disappearing on new messages (#4106)
* Fix highlights from threads disappearing on new messages

This changes interface of Room, so this is a BREAKING CHANGE.

Correctly mirrors the logic we use for room notifications for thread
notifications, ie. set only the total notifications count from the
server if it's zero.

I'm not delighted with this since it ends up with function on room
whose contract is to do something frankly, deeply weird and
unintuitive. However, this is the hack we use for room notifications
and it, empirically, works well enough. To do better, we'd need much
more complex logic to overlay notification counts for decrypted messages.

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

* Add tests for the special notification behaviour in syncing

* Correctly copy the room logic for reseting notifications

We were always ignoring the highlight count, even for encrypted rooms,
which was broken because we don't do the local calculation for unencrypted
rooms.
2024-03-21 16:29:00 +00:00
David Baker e517d009bf Extend logic for local notification processing to threads (#4111)
* Move code for processing our own receipts to Room

This is some code to process our own receipts and recalculate our
notification counts.

There was no reason for this to be in client. Room is still rather
large, but at least it makes somewhat more sense there.

Moving as a refactor before I start work on it.

* Add test for the client-side e2e notifications code

* Extend logic for local notification processing to threads

There's collection of logic for for processing receipts and recomputing
notifications for encrypted rooms, but we didn't do the same for threads.
As a reasult, when I tried pulling some of the logic over in
https://github.com/matrix-org/matrix-js-sdk/pull/4106
clearing notifications on threads just broke.

This extends the logic of reprocessing local notifications when a receipt
arrives to threads.

Based on https://github.com/matrix-org/matrix-js-sdk/pull/4109

* simplify object literal

* Add tests & null guard

* Remove unused imports

* Add another skipped test

* Unused import

* enable tests

* Fix thread support nightmare

* Try this way

* Unused import

* Comment the bear trap

* expand comment
2024-03-21 12:22:19 +00:00
Ajay Bura dc2d03dea5 fix public rooms post request search params and body (#4110) 2024-03-21 10:29:51 +00:00
David Baker d5bb9e7600 Move code for processing our own receipts to Room (#4109)
* Move code for processing our own receipts to Room

This is some code to process our own receipts and recalculate our
notification counts.

There was no reason for this to be in client. Room is still rather
large, but at least it makes somewhat more sense there.

Moving as a refactor before I start work on it.

* Add test for the client-side e2e notifications code

* simplify object literal
2024-03-20 15:20:47 +00:00
Michael Telatynski d908036f50 Improve types for sendStateEvent (#4105) 2024-03-20 14:27:27 +00:00
David Baker afc3c6213b Fix bugs with the first reply to a thread (#4104)
* WIP fix for bugs first-thread-reply bugs

* Add re-emitter before we start adding events, as per comment

* Add test for notification bug

* Test for the bug that caused the dot to be the wrong colour

* Add comment

* elaborate

* Fix outdated comment

* Also fix this comment

* Fix another comment

* Fix typo

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

* Clarify comment

* More comment

* so much comment

also reformat (the bit that's actually added is s/it/this.addEvents/)

* The comments

* Maybe make comment clearer.

* Add comment about potential race

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-20 11:14:25 +00:00
Michael Telatynski 7884c22e41 Fix permissions for deploying docs to github pages (#4122)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-19 17:57:33 +00:00
renovate[bot] 887d8a7663 Update dependency typescript to v5.4.2 (#4123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 17:25:31 +00:00
renovate[bot] 2c68ee2254 Update babel monorepo to v7.24.1 (#4119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:33:19 +00:00
renovate[bot] d445823d0b Update typescript-eslint monorepo to v7.2.0 (#4121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:25:50 +00:00
renovate[bot] abe4630687 Update typedoc (#4118)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:25:07 +00:00
renovate[bot] 8664b66238 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.7.0 (#4120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:24:49 +00:00
renovate[bot] 596826ab4d Update all non-major dependencies (#4116)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:22:03 +00:00
renovate[bot] c8ec5421c7 Update dependency @types/node to v18.19.24 (#4117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:21:55 +00:00
RiotRobot b8078f9916 v31.6.0-rc.0 2024-03-19 15:05:53 +00:00
Andy Balaam 92342c07ed Introduce Membership TS type (take 2) (#4107)
* Introduce Membership TS type

* Adapt the Membership TS type to be an enum

* Add docstrings for KnownMembership and Membership

* Move Membership types into a separate file, exported from types.ts

---------

Co-authored-by: Stanislav Demydiuk <s.demydiuk@gmail.com>
2024-03-18 12:47:23 +00:00
Valere 3e989006aa Migrate own identity local trust to rust crypto (#4090)
* Migrate own identity trust to rust crypto

* Fix gendoc not happy if msk of IDownloadKeyResult has a signature

* add missing mock

* code review

* Code review

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* review move function down in file

* Review gh suggestion

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

* Review gh suggestion

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

* Review: Cleaning tests, renaming

* Review: better comment

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

* Comment paragraphs

* retry until initial  key query is successfull

* review quick nits

* missing mock in test

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-18 08:57:53 +00:00
Kim Brose 8e0ef5ff2c fix automatic DM avatar with functional members (#4017)
* fix automatic DM avatar with functional members

* update comments

* lint

* add tests for functional members

* keep functional members out of the public API

- remove public API for functional members, reverting most of 0ce2d82, f9b41f6, e65fb24
- remove tests for functional members public API c114bf5
- add shared functional members getter for both room name and avatar fallback generation

* filter functional members from more candidates

- remove from hero(es)
- remove from previous members

* add tests for fallback avatars with functional members

* Add docstring for getFunctionalMembers

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

* inline getInvitedAndJoinedFunctionalMemberCount

* update comments for getAvatarFallbackMember

* use correct list of heroes in getAvatarFallbackMember

* remove redundant type annotation

* optimize performance of invitedAndJoinedFunctionalMemberCount

* calculate nonFunctionalMemberCount in one step

instead of iterating redundantly

* clean up functional member tests with review feedback

* lint

* Update src/models/room.ts

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

* apply feedback about comments

* non-functional per review, lint

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-13 17:01:11 +00:00
Michael Telatynski 78d05942a3 Merge remote-tracking branch 'origin/develop' into develop 2024-03-12 18:53:54 +00:00
Michael Telatynski c22a6858c8 Temporarily disable broken step in the release process
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-12 18:53:44 +00:00
RiotRobot 461aeae281 Resetting package fields for development 2024-03-12 18:34:34 +00:00
RiotRobot 0511e313d3 Merge branch 'master' into develop 2024-03-12 18:34:33 +00:00
RiotRobot da3d5c4a43 v31.5.0 2024-03-12 18:33:38 +00:00
renovate[bot] 4c26b55c9a Update dependency typedoc to v0.25.11 (#4102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-12 13:38:32 +00:00
Michael Telatynski 3711ad7e61 Export types describing all specced media event formats (#4092)
* Export types describing all specced media event formats

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

* Iterate PR

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

* Move types to a dedicated export

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

* Iterate

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

* Add readme entry

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-08 21:07:26 +00:00
Michael Telatynski 3031152444 Add .m.rule.is_room_mention push rule to DEFAULT_OVERRIDE_RULES (#4100)
* Add intentional mentions push rules to DEFAULT_OVERRIDE_RULES

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>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-08 20:21:12 +00:00
Daniel Salinas 51ebd2fcde Fix race condition with sliding sync extensions (#4089)
* Fix race condition with sliding sync extensions

* Fix types on sliding-sync spec test

* Prettier fixes
2024-03-07 23:56:36 +00:00
David Baker 27dd856778 Don't re-fetch thread root if we already have it (#4088)
The root event of a thread used to arrive with the pagination request, but this was unspecced and so got changed to simply fetch the root event. In many (almost all) cases this shouldn't be necessary because the thread should already have its root event: re-use it if it's already there. This is only in pagination, so there's no reason to believe that the root event would have changed and needs to be re-fetched.

This removes a number of duplicate calls to the /event/ endpoint from the tests.
2024-03-06 14:10:35 +00:00
renovate[bot] 7fee37680f Update typedoc (#4098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-06 08:49:16 +00:00
renovate[bot] 2541ca04c2 Update all non-major dependencies (#4096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-05 17:54:30 +00:00
Richard van der Hoff d55c6a36df PR template: reminder to document your stuff (#4081)
* PR template: reminder to document your stuff

* link to tsdoc, not typedoc

* add full stops
2024-03-05 17:10:33 +00:00
Timo 8c0736a719 Make sending ContentLoaded optional for a widgetClient (#4086)
* add sendContentLoaded option to widgetClient

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

* review

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

* add tests

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

* another try to get the coverage up

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

* self review

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2024-03-05 16:59:07 +00:00
renovate[bot] 50b042d1ff Update tspascoal/get-user-teams-membership digest to 57e9f42 (#4094)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 16:14:15 +00:00
renovate[bot] a818dc1e9d Update shogo82148/actions-upload-release-asset digest to 5bd52f0 (#4093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:56:46 +00:00
renovate[bot] d8dae65a4d Update typescript-eslint monorepo to v7.1.0 (#4099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:43:37 +00:00
renovate[bot] 8be286308c Update dependency @types/node to v18.19.21 (#4095)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:16:30 +00:00
renovate[bot] 84498bf77d Update babel monorepo to v7.24.0 (#4097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 14:50:28 +00:00
Michael Telatynski a1f4b07b7d Fix bad string split destructuring
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-05 14:31:57 +00:00
RiotRobot ee8413beff v31.5.0-rc.0 2024-03-05 14:03:49 +00:00
Michael Telatynski e4d4628cc8 When merging release notes, allow considering later versions in the same release cycle (#4085)
* When merging release notes, allow considering later versions in the same major.minor.patch set

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

* Tweak comments

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-05 09:34:09 +00:00
David Langley b2e09250d9 Add job to automate adding new issues to the new project (#4087)
* add job to automate adding new issues to the new project

* missing jobs:
2024-02-29 12:20:53 +00:00
RiotRobot 6176faef48 Resetting package fields for development 2024-02-27 12:51:20 +00:00
RiotRobot 453cdd9eda Merge branch 'master' into develop 2024-02-27 12:51:18 +00:00
RiotRobot 6529f02c28 v31.4.0 2024-02-27 12:50:27 +00:00
Valere d3dfcd9242 Add basic retry for rust crypto outgoing requests (#4061)
* Add basic retry for outgoing requests

* Update doc

* Remove 504 from retryable

* Retry all 5xx and clarify client timeouts

* code review cleaning

* do not retry rust request if M_TOO_LARGE

* refactor use common retry alg between scheduler and rust requests

* Code review, cleaning and doc
2024-02-26 14:07:28 +00:00
Michael Telatynski a26fc46ed4 Update MSC2965 OIDC Discovery implementation (#4064) 2024-02-23 16:43:11 +00:00
renovate[bot] be3913e8a5 Update typedoc (#3958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 17:30:14 +00:00
renovate[bot] c1e0192baf Update dependency eslint-plugin-unicorn to v51 (#4083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 17:06:12 +00:00
Andy Balaam 8123e9a3f1 Bump matrix-react-sdk-crypto-wasm to v4.6.0 (#4082)
To reduce memory usage during export - see https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/105
2024-02-22 11:55:27 +00:00
Michael Telatynski 0425f4e5c8 Merge remote-tracking branch 'origin/develop' into develop 2024-02-22 10:49:07 +00:00
Michael Telatynski 5b74b446d4 Prune broken docs symlinks
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-22 10:48:50 +00:00
renovate[bot] 624914a565 Update dependency husky to v9 (#4051)
* Update dependency husky to v9

* Iterate

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-02-22 09:46:30 +00:00
renovate[bot] 4da9627727 Update all non-major dependencies (#4076)
* 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-02-22 09:35:20 +00:00
renovate[bot] 1cb30bfe9b Update typescript-eslint monorepo to v7 (#4078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 09:04:17 +00:00
renovate[bot] 12308b4c07 Update dependency @types/node to v18.19.17 (#4075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 09:02:45 +00:00
Michael Telatynski 7f25162725 Update pull_request.yaml 2024-02-21 21:31:43 +00:00
David Baker 4826868a8f Update stale comment (#4080)
And remove line that set it for it to just get overwritten
2024-02-21 19:01:16 +00:00
renovate[bot] 91bde6afa1 Update dependency typedoc-plugin-coverage to v3 (#4077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 18:45:39 +00:00
RiotRobot 95842c2b91 v31.4.0-rc.0 2024-02-21 17:56:16 +00:00
Michael Telatynski c27c357688 Validate account_management_uri and account_management_actions_supported from OIDC Issuer well-known (#4074)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-21 14:56:11 +00:00
renovate[bot] b474439256 Update dependency oidc-client-ts to v3 (#4052)
* Update dependency oidc-client-ts to v3

* Update jwt-decode so that oidc-client-ts doesn't run its own and thus we can mock

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

* Merge

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

* delint

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

* Sort package.json

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

* Ensure oidc-client-ts 3.0.1 to drop crypto-js

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-02-19 17:20:56 +00:00
Michael Telatynski 42dc498359 Update pull_request.yaml 2024-02-19 13:49:48 +00:00
Michael Telatynski ca914c97e0 Allow specifying OIDC url state parameter for passing data to callback (#4068)
* Allow specifying more OIDC client metadata for dynamic registration

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

* Allow specifying url_state for dynamic oidc client registration

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

* Export NonEmptyArray type

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

* Allow specifying more OIDC client metadata for dynamic registration

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

* Export NonEmptyArray type

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

* Fix test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-19 13:03:53 +00:00
Michael Telatynski f96dac1e5b Saner releases clean up (#4072)
* Remove allchange dependency

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

* Remove stale release scripts

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

* Update pull request template to remove allchange behaviours

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

* Update label check automation

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

* mheap

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

* Re-add node-fetch which was previously transitive via allchange

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

* Use node-fetch@^2

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-19 12:32:26 +00:00
Michael Telatynski 7e0d92cbe0 Add getAuthIssuer method for MSC2965 (#4071)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-19 12:25:24 +00:00
Michael Telatynski fe46fec161 Allow specifying more OIDC client metadata for dynamic registration (#4070) 2024-02-16 14:43:52 +00:00
David Baker 2cf7d819d9 Add unread marker event type (#4069)
* Add unread marker event type

To support setting the 'marked unread' flag

* Await encrypted messages (#4063)

* await encrypted messages
+ fix comments

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

* fix Tests

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

* fix test

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

* make sonar happy

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

---------

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

* Ignore memberships of users that are not in the call (#4065)

* ignore memberships of users that are not in the call

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

* recompute memberships on room member change.

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

* fix Tests and add test for left member

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

* fix event type

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

* fix import desaster

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

* fix mocks

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

---------

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
2024-02-16 10:34:15 +00:00
Timo 74c109adac Ignore memberships of users that are not in the call (#4065)
* ignore memberships of users that are not in the call

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

* recompute memberships on room member change.

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

* fix Tests and add test for left member

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

* fix event type

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

* fix import desaster

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

* fix mocks

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2024-02-14 13:04:40 +00:00
Timo 5d7218476a Await encrypted messages (#4063)
* await encrypted messages
+ fix comments

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

* fix Tests

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

* fix test

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

* make sonar happy

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2024-02-14 11:25:43 +00:00
RiotRobot d03db17405 Resetting package fields for development 2024-02-13 14:56:34 +00:00
RiotRobot 7520340c46 Merge branch 'master' into develop 2024-02-13 14:56:33 +00:00
RiotRobot b63845a413 v31.3.0 2024-02-13 14:55:35 +00:00
Andy Balaam 1b7695cdca Add AsJson forms of the key import/export methods (#4057) 2024-02-08 13:25:22 +00:00
Valere f4a796ca2f ElementR | Ensure own user and device trust are updated after migration before giving back control to the app. (#4059)
* Ensure own trust after olm migration

* Check legacy store contains data
2024-02-07 16:28:17 +00:00
Andy Balaam 58a5d09aed Bump matrix-sdk-crypto-wasm to 4.5.0 (#4060)
Fixes https://github.com/element-hq/element-web/issues/26948
2024-02-07 15:11:04 +00:00
RiotRobot bc620796c3 v31.3.0-rc.4 2024-02-06 15:36:22 +00:00
David Baker b1cfed1b21 Merge pull request #4056 from matrix-org/backport-4055-to-staging
[Backport staging] Add utility to check for non migrated legacy db
2024-02-06 14:09:14 +00:00
Valere c700d8daa2 Add utility to check for non migrated legacy db (#4055)
* Add utility to check for non migrated legacy db

* code review changes

* add unit tests for existsAndIsNotMigrated

* ensure indexeddb is clean for each state

(cherry picked from commit f94dbdec0f)
2024-02-06 13:18:48 +00:00
Valere f94dbdec0f Add utility to check for non migrated legacy db (#4055)
* Add utility to check for non migrated legacy db

* code review changes

* add unit tests for existsAndIsNotMigrated

* ensure indexeddb is clean for each state
2024-02-05 14:59:02 +00:00
renovate[bot] 173d9c331a Update dependency @types/jest to v29.5.12 (#4049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 17:39:51 +00:00
renovate[bot] 04ebcf7be7 Update peter-evans/repository-dispatch action to v3 (#4053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 17:10:27 +00:00
renovate[bot] 5e185ae1e7 Update typescript-eslint monorepo to v6.20.0 (#4050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 17:08:12 +00:00
renovate[bot] 322cc6da10 Update babel monorepo to v7.23.9 (#4047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 16:56:15 +00:00
renovate[bot] 014e674a4e Update definitelyTyped (#4048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 16:55:59 +00:00
renovate[bot] 87acd9dd88 Update all non-major dependencies (#4046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 16:55:36 +00:00
Michael Telatynski bbccb98c06 Fix tag_name
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:26:40 +00:00
Michael Telatynski ca835a7cf7 Work around github actions id clash issue on release
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:22:15 +00:00
Michael Telatynski b8fb10a1d1 Fix merge-release-notes.js
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:09:44 +00:00
Michael Telatynski 5e9d2e064e Debug logging
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:07:20 +00:00
Michael Telatynski d2753a9aea Debug logging
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 16:03:52 +00:00
Michael Telatynski c6eda55110 Fix badly typed output
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:58:48 +00:00
Michael Telatynski 7ce243110f github-script is broken =(
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:55:14 +00:00
Michael Telatynski 20d26db37d Roll back to github-script v6 due to bugs
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:48:58 +00:00
Michael Telatynski a2a25e71ac Revert "Use ELEMENT_BOT_TOKEN for release-drafter-workflow.yml"
This reverts commit 1e7bc2f31c.
2024-02-02 15:44:43 +00:00
Michael Telatynski 1e7bc2f31c Use ELEMENT_BOT_TOKEN for release-drafter-workflow.yml
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-02 15:41:16 +00:00
Michael Telatynski eec5040bd0 Update release-drafter-workflow.yml 2024-02-02 15:36:57 +00:00
Michael Telatynski 24174c9233 Update release-drafter-workflow.yml 2024-02-02 15:33:38 +00:00
Michael Telatynski 8a2cd3f43c Update release-drafter-workflow.yml 2024-02-02 15:31:36 +00:00
Michael Telatynski eebf40590f Update release-drafter-workflow.yml 2024-02-02 15:21:54 +00:00
Michael Telatynski f5e0b3007b Update release-drafter-workflow.yml 2024-02-02 15:18:37 +00:00
Michael Telatynski 45b02fed5a Update release-npm.yml 2024-02-02 14:59:30 +00:00
Michael Telatynski 6edf3990f6 Update release-make.yml 2024-02-02 14:51:49 +00:00
Michael Telatynski 7da620c5be Update release-drafter.yml 2024-02-02 14:40:28 +00:00
237 changed files with 87100 additions and 78783 deletions
-22
View File
@@ -1,22 +0,0 @@
{
"sourceMaps": true,
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": 10
},
"modules": "commonjs"
}
],
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime"
]
}
+1
View File
@@ -0,0 +1 @@
_docs
+16 -3
View File
@@ -49,6 +49,22 @@ module.exports = {
},
],
"no-restricted-properties": [
"error",
{
object: "window",
property: "setImmediate",
message: "Use setTimeout instead.",
},
],
"no-restricted-globals": [
"error",
{
name: "setImmediate",
message: "Use setTimeout instead.",
},
],
"import/no-restricted-paths": [
"error",
{
@@ -103,11 +119,8 @@ module.exports = {
},
},
{
// We don't need amazing docs in our spec files
files: ["src/**/*.ts"],
rules: {
"tsdoc/syntax": "error",
// We use some select jsdoc rules as the tsdoc linter has only one rule
"jsdoc/no-types": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
+4 -9
View File
@@ -2,12 +2,7 @@
## Checklist
- [ ] Tests written for new code (and old code if feasible)
- [ ] 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))
<!--
If you would like to specify text for the changelog entry other than your PR title, add the following:
Notes: Add super cool feature
-->
- [ ] 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@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # 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@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+43
View File
@@ -0,0 +1,43 @@
- name: "A-Element-R"
description: "Issues affecting the port of Element's crypto layer to Rust"
color: "bfd4f2"
- name: "A-Packaging"
description: "Packaging, signing, releasing"
color: "bfd4f2"
- name: "A-Technical-Debt"
color: "bfd4f2"
- name: "A-Testing"
description: "Testing, code coverage, etc."
color: "bfd4f2"
- name: "backport staging"
description: "Label to automatically backport PR to staging branch"
color: "B60205"
- name: "Dependencies"
description: "Pull requests that update a dependency file"
color: "0366d6"
- name: "Easy"
color: "5dc9f7"
- name: "Sponsored"
color: "ffc8f4"
- name: "T-Deprecation"
description: "A pull request that makes something deprecated"
color: "98e6ae"
- name: "T-Other"
description: "Questions, user support, anything else"
color: "98e6ae"
- name: "X-Blocked"
color: "ff7979"
- name: "X-Breaking-Change"
color: "ff7979"
- name: "X-Reverted"
description: "PR has been reverted"
color: "F68AA3"
- name: "X-Upcoming-Release-Blocker"
description: "This does not affect the current release cycle but will affect the next one"
color: "e99695"
- name: "Z-Community-PR"
description: "Issue is solved by a community member's PR"
color: "ededed"
- name: "Z-Flaky-Test"
description: "A test is raising false alarms"
color: "ededed"
@@ -1,22 +0,0 @@
name: Build downstream artifacts
on:
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
# to develop or master.
#
#push:
# branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.90.0
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
@@ -3,10 +3,16 @@
name: matrix-react-sdk End to End Tests
on:
workflow_run:
workflows: ["Build downstream artifacts"]
types:
- completed
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
# to develop or master.
#
#push:
# branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
@@ -15,44 +21,14 @@ concurrency:
jobs:
playwright:
name: Playwright
# We only want to run the playwright tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume multiple concurrent runners.
if: github.event.workflow_run.event == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
permissions:
actions: read
issues: read
statuses: write
pull-requests: read
deployments: write
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# We want to make the Playwright tests a required check for the merge queue.
#
# Unfortunately, GitHub doesn't distinguish between "checks needed for branch
# protection" (ie, the things that must pass before the PR will even be added
# to the merge queue) and "checks needed in the merge queue". We just have to add
# the check to the branch protection list.
#
# Ergo, if we know we're not going to run the Playwright tests, we need to add a
# passing status check manually.
mark_skipped:
if: github.event.workflow_run.event != 'merge_group'
permissions:
statuses: write
runs-on: ubuntu-latest
steps:
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
with:
authToken: "${{ secrets.GITHUB_TOKEN }}"
state: success
description: Playwright skipped
# Keep in step with the `context` that is updated by `Sibz/github-status-action`
# in matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml.
context: "${{ github.workflow }} / end-to-end-tests"
sha: "${{ github.event.workflow_run.head_sha }}"
# We only want to run the playwright tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume multiple concurrent runners.
skip: ${{ github.event_name != 'merge_group' }}
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
- repo: element-hq/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+13 -5
View File
@@ -14,11 +14,18 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
if: github.event_name != 'merge_group'
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
labels: |
X-Breaking-Change
T-Deprecation
T-Enhancement
T-Defect
T-Task
Dependencies
mode: minimum
count: 1
prevent-blocked:
name: Prevent Blocked
@@ -39,7 +46,8 @@ jobs:
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3
if: github.event.pull_request.user.login != 'renovate[bot]'
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
@@ -48,7 +56,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@v7
with:
script: |
@@ -1,4 +1,5 @@
name: Release Drafter
# Workflow used by other workflows to generate draft releases.
name: Release Drafter Reusable
on:
workflow_call:
inputs:
@@ -46,7 +47,8 @@ jobs:
if: inputs.include-changes
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
DEPENDENCY: ${{ inputs.include-changes }}
VERSION: ${{ steps.draft-release.outputs.tag_name }}
with:
@@ -60,7 +62,7 @@ jobs:
if (DEPENDENCY.includes("/")) {
deps.push(DEPENDENCY.replace("$VERSION", VERSION))
} else {
const fromVersion = JSON.parse(await exec.exec("git show origin/master:package.json")).dependencies[DEPENDENCY];
const fromVersion = JSON.parse((await github.request(`https://raw.githubusercontent.com/${owner}/${repo}/master/package.json`)).data).dependencies[DEPENDENCY];
const toVersion = require("./package.json").dependencies[DEPENDENCY];
if (toVersion.endsWith("#develop")) {
@@ -82,5 +84,6 @@ jobs:
repo,
release_id: releaseId,
body: notes,
tag_name: VERSION,
});
}
+4
View File
@@ -1,3 +1,7 @@
# Generates the draft release for the js-sdk
# Normally triggered whenever anything is merged to the staging branch, but
# also has a workflow dispatch trigger in case it needs running manually due
# to failures / workflow updates etc.
name: Release Drafter
on:
push:
+1
View File
@@ -34,6 +34,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
+23 -21
View File
@@ -24,10 +24,6 @@ on:
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
include-changes:
description: Project to include changelog entries from in this release.
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
@@ -58,8 +54,8 @@ jobs:
fingerprint: ${{ inputs.gpg-fingerprint }}
- name: Get draft release
id: release
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
id: draft-release
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -91,7 +87,7 @@ jobs:
jq -e .scripts.dist package.json >/dev/null 2>&1 && HAS_DIST=1
echo "has-dist-script=$HAS_DIST" >> $GITHUB_OUTPUT
env:
VERSION: ${{ steps.release.outputs.tag_name }}
VERSION: ${{ steps.draft-release.outputs.tag_name }}
- name: Finalise version
if: inputs.final
@@ -120,6 +116,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install dependencies
run: "yarn install --frozen-lockfile"
@@ -159,7 +156,7 @@ jobs:
rm CHANGELOG.md.old
git add CHANGELOG.md
env:
RELEASE_NOTES: ${{ steps.release.outputs.body }}
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
- name: Run pre-release script to update package.json fields
run: |
@@ -178,7 +175,7 @@ jobs:
uses: ./.action-repo/.github/actions/upload-release-assets
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.release.outputs.upload_url }}
upload-url: ${{ steps.draft-release.outputs.upload_url }}
asset-path: ${{ inputs.asset-path }}
- name: Create signed tag
@@ -193,7 +190,7 @@ jobs:
uses: ./.action-repo/.github/actions/sign-release-tarball
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.release.outputs.upload_url }}
upload-url: ${{ steps.draft-release.outputs.upload_url }}
# We defer pushing changes until after the release assets are built,
# signed & uploaded to improve the atomicity of this action.
@@ -214,7 +211,7 @@ jobs:
if: inputs.expected-asset-count
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
with:
retries: 3
@@ -242,7 +239,7 @@ jobs:
- name: Publish release
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
FINAL: ${{ inputs.final }}
with:
retries: 3
@@ -294,15 +291,15 @@ jobs:
remove-labels: X-Upcoming-Release-Blocker
add-labels: X-Release-Blocker
- name: Wait for master->develop gitflow merge
if: inputs.final
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with:
ref: master
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-name: merge
allowed-conclusions: success
# - name: Wait for master->develop gitflow merge
# if: inputs.final
# uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
# with:
# ref: master
# repo-token: ${{ secrets.GITHUB_TOKEN }}
# wait-interval: 10
# check-name: merge
# allowed-conclusions: success
bump-downstreams:
name: Update npm dependency in downstream projects
@@ -319,6 +316,11 @@ jobs:
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 }}
+6 -1
View File
@@ -4,6 +4,10 @@ on:
secrets:
NPM_TOKEN:
required: true
outputs:
id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
jobs:
npm:
name: Publish to npm
@@ -21,13 +25,14 @@ jobs:
with:
cache: "yarn"
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552 # v3.0.1
uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c # v3.1.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
+21 -25
View File
@@ -39,39 +39,35 @@ jobs:
- name: 🧮 Checkout code
uses: actions/checkout@v4
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v4
with:
ref: gh-pages
path: _docs
- name: 🔧 Yarn cache
uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
- name: 🔨 Install symlinks
run: |
sudo apt-get update
sudo apt-get install -y symlinks
- name: 📖 Generate docs
run: |
yarn tpv purge --yes --out _docs --stale --major 10
yarn gendoc
symlinks -rc _docs
run: yarn gendoc
- name: 🔨 Set up git
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: _docs
- name: 🚀 Deploy
run: |
git add . --all
git commit -m "Update docs"
git push
working-directory: _docs
docs-deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: docs
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+6 -14
View File
@@ -12,10 +12,6 @@ on:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
extra_args:
type: string
required: false
description: "Extra args to pass to SonarCloud"
jobs:
sonarqube:
runs-on: ubuntu-latest
@@ -34,7 +30,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
@@ -60,14 +56,14 @@ jobs:
- id: extra_args
run: |
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
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')
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.7
uses: matrix-org/sonarcloud-workflow-action@v3.2
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
@@ -78,10 +74,6 @@ jobs:
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
extra_args: |
${{ inputs.extra_args }}
-Dsonar.javascript.lcov.reportPaths=${{ steps.extra_args.outputs.reportPaths }}
-Dsonar.testExecutionReportPaths=${{ steps.extra_args.outputs.testExecutionReportPaths }}
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
if: always()
+22 -8
View File
@@ -18,6 +18,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
@@ -44,6 +45,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
@@ -60,6 +62,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
@@ -76,19 +79,13 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors"
# Upload artifact duplicates symlink contents so we do this to save 75% space
- name: Flatten symlink and write _redirects
run: |
find _docs -mindepth 1 -maxdepth 1 ! -type f ! -name stable -printf '/%f/* /stable/:splat\n' > _docs/_redirects
find _docs -mindepth 1 -maxdepth 1 -type l -delete
find _docs -mindepth 1 -maxdepth 1 -type d -execdir mv {} stable \; -quit
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@v4
@@ -97,3 +94,20 @@ jobs:
path: _docs
# We'll only use this in a workflow_run, then we're done with it
retention-days: 1
analyse_dead_code:
name: "Analyse Dead Code"
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 --frozen-lockfile"
- name: Run linter
run: "yarn run lint:knip"
+21
View File
@@ -0,0 +1,21 @@
name: Sync labels
on:
workflow_dispatch: {}
schedule:
- cron: "0 1 * * *" # 1am every day
push:
branches:
- develop
paths:
- .github/labels.yml
jobs:
sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
with:
LABELS: |
element-hq/element-meta
.github/labels.yml
DELETE: true
WET: true
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+29 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
specs: [integ, unit]
node: [18, "lts/*", 21]
node: ["lts/*", 22]
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -63,6 +63,16 @@ jobs:
coverage
!coverage/lcov-report
# Dummy completion job to simplify branch protections
jest-complete:
name: Jest tests
needs: jest
if: always()
runs-on: ubuntu-latest
steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1
matrix-react-sdk:
name: Downstream test matrix-react-sdk
if: github.event_name == 'merge_group'
@@ -71,6 +81,24 @@ jobs:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
with:
use_js_sdk: "."
# we need this so the job is reported properly when run in a merge queue
downstream-complement-crypto:
name: Downstream Complement Crypto tests
runs-on: ubuntu-latest
if: always()
needs:
- complement-crypto
steps:
- if: needs.complement-crypto.result != 'skipped' && needs.complement-crypto.result != 'success'
run: exit 1
# Hook for branch protection to skip downstream testing outside of merge queues
# and skip sonarcloud coverage within merge queues
downstream:
+14
View File
@@ -0,0 +1,14 @@
name: Move new issues into Issue triage board
on:
issues:
types: [opened]
jobs:
automate-project-columns-next:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -6,6 +6,6 @@ on:
jobs:
call-triage-labelled:
uses: vector-im/element-web/.github/workflows/triage-labelled.yml@develop
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+252
View File
@@ -1,3 +1,255 @@
Changes in [34.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.3.0) (2024-08-13)
==================================================================================================
## ✨ Features
* Bump matrix-widget-api ([#4336](https://github.com/matrix-org/matrix-js-sdk/pull/4336)). Contributed by @AndrewFerr.
* Also check for MSC3757 for session state keys ([#4334](https://github.com/matrix-org/matrix-js-sdk/pull/4334)). Contributed by @AndrewFerr.
* Support Futures via widgets ([#4311](https://github.com/matrix-org/matrix-js-sdk/pull/4311)). Contributed by @AndrewFerr.
* Support MSC4140: Delayed events (Futures) ([#4294](https://github.com/matrix-org/matrix-js-sdk/pull/4294)). Contributed by @AndrewFerr.
* Handle late-arriving `m.room_key.withheld` messages ([#4310](https://github.com/matrix-org/matrix-js-sdk/pull/4310)). Contributed by @richvdh.
* Be specific about what is considered a MSC4143 call member event. ([#4328](https://github.com/matrix-org/matrix-js-sdk/pull/4328)). Contributed by @toger5.
* Add index.ts for matrixrtc module ([#4314](https://github.com/matrix-org/matrix-js-sdk/pull/4314)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix hashed ID server lookups with no Olm ([#4333](https://github.com/matrix-org/matrix-js-sdk/pull/4333)). Contributed by @dbkr.
Changes in [34.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.2.0) (2024-07-30)
==================================================================================================
## 🐛 Bug Fixes
* Element-R: detect "withheld key" UTD errors, and mark them as such ([#4302](https://github.com/matrix-org/matrix-js-sdk/pull/4302)). Contributed by @richvdh.
Changes in [34.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.1.0) (2024-07-16)
==================================================================================================
## ✨ Features
* Add ability to choose how many timeline events to sync when peeking ([#4300](https://github.com/matrix-org/matrix-js-sdk/pull/4300)). Contributed by @jgarplind.
* Remove redundant hack for using the old pickle key in rust crypto ([#4282](https://github.com/matrix-org/matrix-js-sdk/pull/4282)). Contributed by @richvdh.
* Add fetching the well known in embedded mode. ([#4259](https://github.com/matrix-org/matrix-js-sdk/pull/4259)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix room state being updated with old (now overwritten) state and emitting for those updates. ([#4242](https://github.com/matrix-org/matrix-js-sdk/pull/4242)). Contributed by @toger5.
* Fix incorrect "Olm is not available" errors ([#4301](https://github.com/matrix-org/matrix-js-sdk/pull/4301)). Contributed by @richvdh.
* Fix build for example script ([#4286](https://github.com/matrix-org/matrix-js-sdk/pull/4286)). Contributed by @richvdh.
* Declare matrix-js-sdk as an ES module ([#4285](https://github.com/matrix-org/matrix-js-sdk/pull/4285)). Contributed by @richvdh.
Changes in [34.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.0.0) (2024-07-08)
==================================================================================================
## 🚨 BREAKING CHANGES
* Fetch capabilities in the background ([#4246](https://github.com/matrix-org/matrix-js-sdk/pull/4246)). Contributed by @dbkr.
## ✨ Features
* Prefix the user+device state key if needed ([#4262](https://github.com/matrix-org/matrix-js-sdk/pull/4262)). Contributed by @AndrewFerr.
* Use legacy call membership if anyone else is ([#4260](https://github.com/matrix-org/matrix-js-sdk/pull/4260)). Contributed by @AndrewFerr.
* Fetch capabilities in the background ([#4246](https://github.com/matrix-org/matrix-js-sdk/pull/4246)). Contributed by @dbkr.
* Use server name instead of homeserver url to allow well-known lookups during QR OIDC reciprocation ([#4233](https://github.com/matrix-org/matrix-js-sdk/pull/4233)). Contributed by @t3chguy.
* Add via parameter for MSC4156 ([#4247](https://github.com/matrix-org/matrix-js-sdk/pull/4247)). Contributed by @Johennes.
* Make the js-sdk compatible with MSC preferred foci and active focus. ([#4195](https://github.com/matrix-org/matrix-js-sdk/pull/4195)). Contributed by @toger5.
* Replace usages of setImmediate with setTimeout for wider compatibility ([#4240](https://github.com/matrix-org/matrix-js-sdk/pull/4240)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* [Backport staging] Fix "Unable to restore session" error ([#4299](https://github.com/matrix-org/matrix-js-sdk/pull/4299)). Contributed by @RiotRobot.
* [Backport staging] Fix error when sending encrypted messages in large rooms ([#4297](https://github.com/matrix-org/matrix-js-sdk/pull/4297)). Contributed by @RiotRobot.
* Element-R: Fix resource leaks in verification logic ([#4263](https://github.com/matrix-org/matrix-js-sdk/pull/4263)). Contributed by @richvdh.
* Upgrade Rust Crypto SDK to 6.1.0 ([#4261](https://github.com/matrix-org/matrix-js-sdk/pull/4261)). Contributed by @richvdh.
* Correctly transform base64 with multiple instances of + or / ([#4252](https://github.com/matrix-org/matrix-js-sdk/pull/4252)). Contributed by @robintown.
* Work around spec bug for m.room.avatar state event content type ([#4245](https://github.com/matrix-org/matrix-js-sdk/pull/4245)). Contributed by @t3chguy.
Changes in [33.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v33.1.0) (2024-06-18)
==================================================================================================
## ✨ Features
* MSC4108 support OIDC QR code login ([#4134](https://github.com/matrix-org/matrix-js-sdk/pull/4134)). Contributed by @t3chguy.
* Add crypto methods for export and import of secrets bundle ([#4227](https://github.com/matrix-org/matrix-js-sdk/pull/4227)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Fix screen sharing in recent Chrome ([#4243](https://github.com/matrix-org/matrix-js-sdk/pull/4243)). Contributed by @RiotRobot.
* Fix incorrect assumptions about required fields in /search response ([#4228](https://github.com/matrix-org/matrix-js-sdk/pull/4228)). Contributed by @t3chguy.
* Fix the queueToDevice tests for the new fakeindexeddb ([#4225](https://github.com/matrix-org/matrix-js-sdk/pull/4225)). Contributed by @dbkr.
Changes in [33.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v33.0.0) (2024-06-04)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove more deprecated methods, fields, and exports ([#4217](https://github.com/matrix-org/matrix-js-sdk/pull/4217)). Contributed by @t3chguy.
* Remove deprecated methods and fields ([#4201](https://github.com/matrix-org/matrix-js-sdk/pull/4201)). Contributed by @t3chguy.
## 🦖 Deprecations
* Remove more deprecated methods, fields, and exports ([#4217](https://github.com/matrix-org/matrix-js-sdk/pull/4217)). Contributed by @t3chguy.
* Remove deprecated methods and fields ([#4201](https://github.com/matrix-org/matrix-js-sdk/pull/4201)). Contributed by @t3chguy.
## ✨ Features
* `initRustCrypto`: allow app to pass in the store key directly ([#4210](https://github.com/matrix-org/matrix-js-sdk/pull/4210)). Contributed by @richvdh.
* Preserve ESM for async imports to work correctly ([#4187](https://github.com/matrix-org/matrix-js-sdk/pull/4187)). Contributed by @ms-dosx86.
## 🐛 Bug Fixes
* Don't run migration for Rust crypto if the legacy store is empty ([#4218](https://github.com/matrix-org/matrix-js-sdk/pull/4218)). Contributed by @andybalaam.
* Bump matrix-sdk-crypto-wasm to 5.0.0 ([#4216](https://github.com/matrix-org/matrix-js-sdk/pull/4216)). Contributed by @richvdh.
* Wire up verification cancel \& mismatch for rust crypto ([#4202](https://github.com/matrix-org/matrix-js-sdk/pull/4202)). Contributed by @t3chguy.
* Only pass id\_server if we had one to begin with ([#4200](https://github.com/matrix-org/matrix-js-sdk/pull/4200)). Contributed by @t3chguy.
Changes in [32.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.4.0) (2024-05-22)
==================================================================================================
* No changes
Changes in [32.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.3.0) (2024-05-21)
==================================================================================================
## ✨ Features
* Simplify OIDC types \& export `decodeIdToken` ([#4193](https://github.com/matrix-org/matrix-js-sdk/pull/4193)). Contributed by @t3chguy.
* Add helpers for authenticated media, and associated documentation ([#4185](https://github.com/matrix-org/matrix-js-sdk/pull/4185)). Contributed by @turt2live.
## 🐛 Bug Fixes
* Fix state\_events.ts types ([#4196](https://github.com/matrix-org/matrix-js-sdk/pull/4196)). Contributed by @t3chguy.
* Fix sendEventHttpRequest for `m.room.redaction` events without `redacts` ([#4192](https://github.com/matrix-org/matrix-js-sdk/pull/4192)). Contributed by @t3chguy.
Changes in [32.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.2.0) (2024-05-07)
==================================================================================================
## ✨ Features
* Use a different error code for UTDs when user was not in the room ([#4172](https://github.com/matrix-org/matrix-js-sdk/pull/4172)). Contributed by @uhoreg.
* Modernize window.crypto access constants ([#4169](https://github.com/matrix-org/matrix-js-sdk/pull/4169)). Contributed by @turt2live.
* Improve compliance with MSC3266 ([#4155](https://github.com/matrix-org/matrix-js-sdk/pull/4155)). Contributed by @AndrewFerr.
* Add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk ([#4152](https://github.com/matrix-org/matrix-js-sdk/pull/4152)). Contributed by @toger5.
* Crypto: use a new error code for UTDs from device-relative historical events ([#4139](https://github.com/matrix-org/matrix-js-sdk/pull/4139)). Contributed by @richvdh.
## 🐛 Bug Fixes
* Element-R: Fix rust migration when ssss secret are stored not encryted in cache (old legacy behavior) ([#4168](https://github.com/matrix-org/matrix-js-sdk/pull/4168)). Contributed by @BillCarsonFr.
Changes in [32.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.1.0) (2024-04-23)
==================================================================================================
## ✨ Features
* Add support for device dehydration v2 (Element R) ([#4062](https://github.com/matrix-org/matrix-js-sdk/pull/4062)). Contributed by @uhoreg.
* OIDC improvements in prep of OIDC-QR reciprocation ([#4149](https://github.com/matrix-org/matrix-js-sdk/pull/4149)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Validate backup private key before migrating it ([#4114](https://github.com/matrix-org/matrix-js-sdk/pull/4114)). Contributed by @BillCarsonFr.
* ElementR| Retry query backup until it works during migration to avoid spurious correption error popup ([#4113](https://github.com/matrix-org/matrix-js-sdk/pull/4113)). Contributed by @BillCarsonFr.
Changes in [32.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.0.0) (2024-04-09)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
## ✨ Features
* Add new `decryptExistingEvent` test helper ([#4133](https://github.com/matrix-org/matrix-js-sdk/pull/4133)). Contributed by @richvdh.
* Improve types for `sendEvent` ([#4108](https://github.com/matrix-org/matrix-js-sdk/pull/4108)). Contributed by @t3chguy.
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
* Add new enum for verification methods. ([#4129](https://github.com/matrix-org/matrix-js-sdk/pull/4129)). Contributed by @richvdh.
* Add some test utils in a new entrypoint ([#4127](https://github.com/matrix-org/matrix-js-sdk/pull/4127)). Contributed by @richvdh.
* Improve types for `sendStateEvent` ([#4105](https://github.com/matrix-org/matrix-js-sdk/pull/4105)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Improve types for `IPowerLevelsContent` and `hasSufficientPowerLevelFor` ([#4128](https://github.com/matrix-org/matrix-js-sdk/pull/4128)). Contributed by @galash13.
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
* Extend logic for local notification processing to threads ([#4111](https://github.com/matrix-org/matrix-js-sdk/pull/4111)). Contributed by @dbkr.
* Fix public rooms post request search params and body ([#4110](https://github.com/matrix-org/matrix-js-sdk/pull/4110)). Contributed by @ajbura.
* Fix bugs with the first reply to a thread ([#4104](https://github.com/matrix-org/matrix-js-sdk/pull/4104)). Contributed by @dbkr.
Changes in [31.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.1) (2024-03-28)
==================================================================================================
## 🐛 Bug Fixes
* Fix merging of default push rules ([#4136](https://github.com/matrix-org/matrix-js-sdk/pull/4136)).
Changes in [31.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.0) (2024-03-26)
==================================================================================================
## ✨ Features
* Introduce Membership TS type (take 2) ([#4107](https://github.com/matrix-org/matrix-js-sdk/pull/4107)). Contributed by @andybalaam.
* fix automatic DM avatar with functional members ([#4017](https://github.com/matrix-org/matrix-js-sdk/pull/4017)). Contributed by @HarHarLinks.
* Export types describing all specced media event formats ([#4092](https://github.com/matrix-org/matrix-js-sdk/pull/4092)). Contributed by @t3chguy.
* Add `.m.rule.is_room_mention` push rule to DEFAULT\_OVERRIDE\_RULES ([#4100](https://github.com/matrix-org/matrix-js-sdk/pull/4100)). Contributed by @t3chguy.
* Make sending ContentLoaded optional for a widgetClient ([#4086](https://github.com/matrix-org/matrix-js-sdk/pull/4086)). Contributed by @toger5.
## 🐛 Bug Fixes
* Migrate own identity local trust to rust crypto ([#4090](https://github.com/matrix-org/matrix-js-sdk/pull/4090)). Contributed by @BillCarsonFr.
* Fix race condition with sliding sync extensions ([#4089](https://github.com/matrix-org/matrix-js-sdk/pull/4089)). Contributed by @zzorba.
Changes in [31.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.5.0) (2024-03-12)
==================================================================================================
## ✨ Features
* Update MSC2965 OIDC Discovery implementation ([#4064](https://github.com/matrix-org/matrix-js-sdk/pull/4064)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Add basic retry for rust crypto outgoing requests ([#4061](https://github.com/matrix-org/matrix-js-sdk/pull/4061)). Contributed by @BillCarsonFr.
Changes in [31.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.4.0) (2024-02-27)
==================================================================================================
## ✨ Features
* Validate `account_management_uri` and `account_management_actions_supported` from OIDC Issuer well-known ([#4074](https://github.com/matrix-org/matrix-js-sdk/pull/4074)). Contributed by @t3chguy.
* Allow specifying OIDC url state parameter for passing data to callback ([#4068](https://github.com/matrix-org/matrix-js-sdk/pull/4068)). Contributed by @t3chguy.
* Add getAuthIssuer method for MSC2965 ([#4071](https://github.com/matrix-org/matrix-js-sdk/pull/4071)). Contributed by @t3chguy.
* Allow specifying more OIDC client metadata for dynamic registration ([#4070](https://github.com/matrix-org/matrix-js-sdk/pull/4070)). Contributed by @t3chguy.
* Add unread marker event type ([#4069](https://github.com/matrix-org/matrix-js-sdk/pull/4069)). Contributed by @dbkr.
* Add "AsJson" forms of the key import/export methods ([#4057](https://github.com/matrix-org/matrix-js-sdk/pull/4057)). Contributed by @andybalaam.
## 🐛 Bug Fixes
* Ignore memberships of users that are not in the call ([#4065](https://github.com/matrix-org/matrix-js-sdk/pull/4065)). Contributed by @toger5.
* Await encrypted messages ([#4063](https://github.com/matrix-org/matrix-js-sdk/pull/4063)). Contributed by @toger5.
* ElementR | Ensure own user and device trust are updated after migration before giving back control to the app. ([#4059](https://github.com/matrix-org/matrix-js-sdk/pull/4059)). Contributed by @BillCarsonFr.
* Bump matrix-sdk-crypto-wasm to 4.5.0 ([#4060](https://github.com/matrix-org/matrix-js-sdk/pull/4060)). Contributed by @andybalaam.
Changes in [31.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.3.0) (2024-02-13)
==================================================================================================
## ✨ Features
* Add expire\_ts compatibility to matrixRTC ([#4032](https://github.com/matrix-org/matrix-js-sdk/pull/4032)). Contributed by @toger5.
* Element-R: support for migration of the room list from legacy crypto ([#4036](https://github.com/matrix-org/matrix-js-sdk/pull/4036)). Contributed by @richvdh.
* Element-R: check persistent room list for encryption config ([#4035](https://github.com/matrix-org/matrix-js-sdk/pull/4035)). Contributed by @richvdh.
* Support optional MSC3860 redirects ([#4007](https://github.com/matrix-org/matrix-js-sdk/pull/4007)). Contributed by @turt2live.
## 🐛 Bug Fixes
* WebR: migrate the megolm session imported flag ([#4037](https://github.com/matrix-org/matrix-js-sdk/pull/4037)). Contributed by @BillCarsonFr.
* ElementR: fix emoji verification stalling when both ends hit start at the same time ([#4004](https://github.com/matrix-org/matrix-js-sdk/pull/4004)). Contributed by @uhoreg.
* Dependencies: Bump wasm bindings version to 4.3.0 ([#4042](https://github.com/matrix-org/matrix-js-sdk/pull/4042)). Contributed by @BillCarsonFr.
* Element R: emit events when devices have changed ([#4019](https://github.com/matrix-org/matrix-js-sdk/pull/4019)). Contributed by @uhoreg.
* ElementR: report invalid keys rather than failing to restore from backup ([#4006](https://github.com/matrix-org/matrix-js-sdk/pull/4006)). Contributed by @uhoreg.
* Make `timeline` a getter ([#4022](https://github.com/matrix-org/matrix-js-sdk/pull/4022)). Contributed by @florianduros.
* Implement getting verification cancellation info in Rust crypto ([#3947](https://github.com/matrix-org/matrix-js-sdk/pull/3947)). Contributed by @uhoreg.
* Fix crypto migration for megolm sessions with no sender key ([#4024](https://github.com/matrix-org/matrix-js-sdk/pull/4024)). Contributed by @richvdh.
Changes in [31.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.2.0) (2024-01-31)
==================================================================================================
## ✨ Features
+60 -57
View File
@@ -21,15 +21,9 @@ endpoints from before Matrix 1.1, for example.
# Quickstart
## In a browser
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
## In Node.js
Ensure you have the latest LTS version of Node.js installed.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
> [!IMPORTANT]
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
@@ -45,9 +39,7 @@ 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) for a more complex example.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
[the Node.js terminal app](examples/node/README.md) for a more complex example.
To start the client:
@@ -101,12 +93,40 @@ Object.keys(client.store.rooms).forEach((roomId) => {
});
```
## Authenticated media
Servers supporting [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) (Matrix 1.11) will require clients, like
yours, to include an `Authorization` header when `/download`ing or `/thumbnail`ing media. For NodeJS environments this
may be as easy as the following code snippet, though web browsers may need to use [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
to append the header when using the endpoints in `<img />` elements and similar.
```javascript
const downloadUrl = client.mxcUrlToHttp(
/*mxcUrl=*/ "mxc://example.org/abc123", // the MXC URI to download/thumbnail, typically from an event or profile
/*width=*/ undefined, // part of the thumbnail API. Use as required.
/*height=*/ undefined, // part of the thumbnail API. Use as required.
/*resizeMethod=*/ undefined, // part of the thumbnail API. Use as required.
/*allowDirectLinks=*/ false, // should generally be left `false`.
/*allowRedirects=*/ true, // implied supported with authentication
/*useAuthentication=*/ true, // the flag we're after in this example
);
const img = await fetch(downloadUrl, {
headers: {
Authorization: `Bearer ${client.getAccessToken()}`,
},
});
// Do something with `img`.
```
> [!WARNING]
> In future the js-sdk will _only_ return authentication-required URLs, mandating population of the `Authorization` header.
## What does this SDK do?
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 `/initialSync` and `/events`)
- 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
@@ -127,20 +147,20 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
Later versions of the SDK will:
- Expose a `RoomSummary` which would be suitable for a recents page.
- Provide different pluggable storage layers (e.g. local storage, database-backed)
# Usage
## Conventions
## Supported platforms
### Emitted events
`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed),
or in browser applications, via a bundler such as Webpack or Vite.
The SDK will emit events using an `EventEmitter`. It also
emits object models (e.g. `Rooms`, `RoomMembers`) when they
are updated.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
## Emitted events
The SDK raises notifications to the application using
[`EventEmitter`s](https://nodejs.org/api/events.html#class-eventemitter). The `MatrixClient` itself
implements `EventEmitter`, as do many of the high-level abstractions such as `Room` and `RoomMember`.
```javascript
// Listen for low-level MatrixEvents
@@ -161,41 +181,21 @@ client.on(RoomMemberEvent.Typing, function (event, member) {
client.startClient();
```
### Promises and Callbacks
## Entry points
Most of the methods in the SDK are asynchronous: they do not directly return a
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
which will be fulfilled in the future.
As well as the primary entry point (`matrix-js-sdk`), there are several other entry points which may be useful:
The typical usage is something like:
```javascript
matrixClient.someMethod(arg1, arg2).then(function(result) {
...
});
```
Alternatively, if you have a Node.js-style `callback(err, result)` function,
you can pass the result of the promise into it with something like:
```javascript
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
The main thing to note is that it is problematic to discard the result of a
promise-returning function, as that will cause exceptions to go unobserved.
Methods which return a promise show this in their documentation.
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
via an optional `callback` argument. The callback support is now deprecated:
new methods do not include a `callback` argument, and in the future it may be
removed from existing methods.
| Entry point | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
| `matrix-js-sdk` | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. |
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
## Examples
This section provides some useful code snippets which demonstrate the
core functionality of the SDK. These examples assume the SDK is setup like this:
core functionality of the SDK. These examples assume the SDK is set up like this:
```javascript
import * as sdk from "matrix-js-sdk";
@@ -211,10 +211,10 @@ const matrixClient = sdk.createClient({
### Automatically join rooms when invited
```javascript
matrixClient.on(RoomMemberEvent.Membership, function (event, member) {
if (member.membership === "invite" && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).then(function () {
console.log("Auto-joined %s", member.roomId);
matrixClient.on(RoomEvent.MyMembership, function (room, membership, prevMembership) {
if (membership === KnownMembership.Invite) {
matrixClient.joinRoom(room.roomId).then(function () {
console.log("Auto-joined %s", room.roomId);
});
}
});
@@ -294,7 +294,7 @@ host the API reference from the source files like this:
```
$ yarn gendoc
$ cd _docs
$ cd docs
$ python -m http.server 8005
```
@@ -302,6 +302,9 @@ 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).
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.
+26
View File
@@ -0,0 +1,26 @@
module.exports = {
sourceMaps: true,
presets: [
[
"@babel/preset-env",
{
targets: {
esmodules: true,
},
// We want to output ES modules for the final build (mostly to ensure that
// async imports work correctly). However, jest doesn't support ES modules very
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
// when testing.
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
},
],
"@babel/preset-typescript",
],
plugins: [
"@babel/plugin-transform-numeric-separator",
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
],
};
+1 -1
View File
@@ -115,7 +115,7 @@ rl.on("line", function (line) {
if (line.indexOf("/join ") === 0) {
var roomIndex = line.split(" ")[1];
viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") {
if (viewingRoom.getMember(myUserId).membership === KnownMembership.Invite) {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).then(
function (room) {
+2 -4
View File
@@ -3,12 +3,10 @@
"version": "0.0.0",
"description": "",
"main": "app.js",
"scripts": {
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0",
"dependencies": {
"cli-color": "^1.0.0"
"cli-color": "^1.0.0",
"matrix-js-sdk": "^32.0.0"
}
}
+36
View File
@@ -0,0 +1,36 @@
import { KnipConfig } from "knip";
export default {
entry: [
"src/index.ts",
"src/types.ts",
"src/browser-index.ts",
"src/indexeddb-worker.ts",
"scripts/**",
"spec/**",
"release.sh",
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
"src/**",
],
project: ["**/*.{js,ts}"],
ignore: ["examples/**"],
ignoreDependencies: [
// Required for `action-validator`
"@action-validator/*",
// Used for git pre-commit hooks
"husky",
// Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest",
// Used by jest
"jest-environment-jsdom",
"babel-jest",
"ts-node",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
],
ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml`
"dist",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;
+34 -36
View File
@@ -1,9 +1,9 @@
{
"name": "matrix-js-sdk",
"version": "31.3.0-rc.3",
"version": "34.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
},
"scripts": {
"prepack": "yarn build",
@@ -19,6 +19,7 @@
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
@@ -52,81 +53,78 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.3.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^3.1.2",
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.6.0",
"oidc-client-ts": "^2.2.4",
"matrix-widget-api": "^1.8.2",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "9"
"uuid": "10"
},
"devDependencies": {
"@action-validator/cli": "^0.5.3",
"@action-validator/core": "^0.5.3",
"@action-validator/cli": "^0.6.0",
"@action-validator/core": "^0.6.0",
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-class-properties": "^7.12.1",
"@babel/plugin-transform-numeric-separator": "^7.12.7",
"@babel/plugin-transform-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"allchange": "^1.3.0",
"@types/uuid": "10",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"babel-jest": "^29.0.0",
"debug": "^4.3.4",
"domexception": "^4.0.0",
"eslint": "8.56.0",
"eslint": "8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^48.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^50.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-unicorn": "^54.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "9.11.0",
"fetch-mock": "10.1.0",
"fetch-mock-jest": "^1.5.1",
"husky": "^8.0.3",
"husky": "^9.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"knip": "^5.0.0",
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"prettier": "3.2.4",
"rimraf": "^5.0.0",
"ts-node": "^10.9.1",
"typedoc": "^0.24.0",
"typedoc-plugin-coverage": "^2.1.0",
"node-fetch": "^2.7.0",
"prettier": "3.3.3",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.26.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^2.0.0",
"typedoc-plugin-versions": "^0.2.3",
"typedoc-plugin-versions-cli": "^0.1.12",
"typescript": "^5.0.0"
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.3.3"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
#
# Script to perform a post-release steps of matrix-js-sdk.
#
# Requires:
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
"$(dirname "$0")/scripts/release/post-merge-master.sh"
git push origin develop
fi
-346
View File
@@ -1,346 +0,0 @@
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk and downstream projects.
#
# Requires:
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
exit
fi
else
echo "hub is required: please install it"
exit
fi
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
help() {
cat <<EOF
$USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
EOF
}
if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit
fi
if ! git diff-files --quiet; then
echo "this git checkout has uncommitted changes. Refusing to release."
exit
fi
skip_changelog=
changelog_file="CHANGELOG.md"
while getopts hc:x f; do
case $f in
h)
help
exit 0
;;
c)
changelog_file="$OPTARG"
;;
x)
skip_changelog=1
;;
esac
done
shift $(expr $OPTIND - 1)
if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2
exit 1
fi
function check_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Checking version of $1..."
local latestver=$(yarn info -s "$1" dist-tags.next)
if [ "$depver" != "$latestver" ]
then
echo "The latest version of $1 is $latestver but package.json depends on $depver."
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
read resp
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
then
echo "Aborting."
exit 1
fi
if [ "$resp" == "u" ]
then
echo "Upgrading $1 to $latestver..."
yarn add -E "$1@$latestver"
git add -u
git commit -m "Upgrade $1 to $latestver"
fi
fi
}
function reset_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Resetting $1 to develop branch..."
yarn add "github:matrix-org/$1#develop"
git add -u
git commit -m "Reset $1 back to develop branch"
}
has_subprojects=0
if [ -f release_config.yaml ]; then
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
if [ "$?" -eq 0 ]; then
has_subprojects=1
echo "Checking subprojects for upgrades"
for proj in $subprojects; do
check_dependency "$proj"
done
fi
fi
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --frozen-lockfile
# ignore leading v on release
release="${1#v}"
tag="v${release}"
prerelease=0
# We check if this build is a prerelease by looking to
# see if the version has a hyphen in it. Crude,
# but semver doesn't support postreleases so anything
# with a hyphen is a prerelease.
echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
else
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
fi
rel_branch=$(git symbolic-ref --short HEAD)
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
yarn run allchange "$release"
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
echo "Committing updated changelog"
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=$(mktemp)
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
set -x
# Bump package.json and build the dist
echo "yarn version"
# yarn version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
yarn version --no-git-tag-version --new-version "$release"
"$(dirname "$0")/scripts/release/pre-release.sh"
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
then
pkglock='yarn.lock'
else
pkglock=''
fi
git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
fi
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from yarn link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=$(pwd)
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
yarn install --frozen-lockfile
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" yarn dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
if [ -n "$signing_id" ]
then
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
assets="$assets -a $i.asc"
fi
done
fi
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag and the release branch
git push origin "$rel_branch" "$tag"
if [ -n "$signing_id" ]; then
# make a signature for the source tarball.
#
# github will make us a tarball from the tag - we want to create a
# signature for it, which means that first of all we need to check that
# it's correct.
#
# we can't deterministically build exactly the same tarball, due to
# differences in gzip implementation - but we *can* build the same tar - so
# the easiest way to check the validity of the tarball from git is to unzip
# it and compare it with our own idea of what the tar should look like.
# This uses git archive which seems to be what github uses. Specifically,
# the header fields are set in the same way: same file mode, uid & gid
# both zero and mtime set to the timestamp of the commit that the tag
# references. Also note that this puts the commit into the tar headers
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
# the name of the sig file we want to create
source_sigfile="${tag}-src.tar.gz.asc"
tarfile="$tag.tar.gz"
gh_project_url=$(git remote get-url origin |
sed -e 's#^git@github\.com:#https://github.com/#' \
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
-e 's/\.git$//')
project_name="${gh_project_url##*/}"
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
# unzip it and compare it with the tar we would generate
if ! cmp --silent <(gunzip -c $tarfile) \
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
# we don't bail out here, because really it's more likely that our comparison
# screwed up and it's super annoying to abort the script at this point.
cat >&2 <<EOF
!!!!!!!!!!!!!!!!!
!!!! WARNING !!!!
Mismatch between our own tarfile and that generated by github: not signing
source tarball.
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
attach it to the release as $source_sigfile.
!!!!!!!!!!!!!!!!!
EOF
else
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
assets="$assets -a $source_sigfile"
fi
fi
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=$(mktemp)
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -F "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
rm "${release_text}"
rm "${latest_changes}"
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
exit 0
fi
# merge release branch to master
echo "updating master branch"
git checkout master
git pull
git merge "$rel_branch" --no-edit
# push master to github
git push origin master
# finally, merge master back onto develop (if it exists)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
git checkout develop
git pull
git merge master --no-edit
git push origin develop
fi
[ -x ./post-release.sh ] && ./post-release.sh
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
echo "Resetting subprojects to develop"
for proj in $subprojects; do
reset_dependency "$proj"
done
git push origin develop
fi
+49 -19
View File
@@ -2,21 +2,29 @@
const fs = require("fs");
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
// Or it can be a string in the form owner/repo@tag
// Or it can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
async function listReleases(github, owner, repo) {
const response = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100,
});
// Filters out draft releases
return response.data.filter((release) => !release.draft);
}
// Dependency can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
// Or it can be a string in the form accepted by `getRelease`
async function getReleases(github, dependency) {
if (Array.isArray(dependency)) {
const [dep, fromVersion, toVersion] = dependency;
const upstreamPackageJson = getDependencyPackageJson(dep);
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
const response = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100,
});
const releases = response.data.filter((release) => !release.draft && !release.prerelease);
const unfilteredReleases = await listReleases(github, owner, repo);
// Only include non-draft & non-prerelease releases, unless the to-release is a pre-release, include that one
const releases = unfilteredReleases.filter(
(release) => !release.prerelease || release.tag_name === `v${toVersion}`,
);
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
const toVersionIndex = releases.findIndex((release) => release.tag_name === `v${toVersion}`);
@@ -27,14 +35,35 @@ async function getReleases(github, dependency) {
return [await getRelease(github, dependency)];
}
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
// Or it can be a string in the form owner/repo@tag - in this case the tag is used exactly to find the release
// Or it can be a string in the form owner/repo~tag - in this case the latest tag in the same major.minor.patch set is used to find the release
async function getRelease(github, dependency) {
let owner;
let repo;
let tag;
if (dependency.includes("/") && dependency.includes("@")) {
owner = dependency.split("/")[0];
repo = dependency.split("/")[1].split("@")[0];
tag = dependency.split("@")[1];
if (dependency.includes("/")) {
let rest;
[owner, rest] = dependency.split("/");
if (dependency.includes("@")) {
[repo, tag] = rest.split("@");
} else if (dependency.includes("~")) {
[repo, tag] = rest.split("~");
if (tag.includes("-rc.")) {
// If the tag is an RC, find the latest matching RC in the set
try {
const releases = await listReleases(github, owner, repo);
const baseVersion = tag.split("-rc.")[0];
const release = releases.find((release) => release.tag_name.startsWith(baseVersion));
if (release) return release;
} catch (e) {
// Fall back to getReleaseByTag
}
}
}
} else {
const upstreamPackageJson = getDependencyPackageJson(dependency);
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
@@ -83,7 +112,14 @@ const main = async ({ github, releaseId, dependencies }) => {
const { GITHUB_REPOSITORY } = process.env;
const [owner, repo] = GITHUB_REPOSITORY.split("/");
const { data: release } = await github.rest.repos.getRelease({
owner,
repo,
release_id: releaseId,
});
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
parseReleaseNotes(release.body, sections);
for (const dependency of dependencies) {
const releases = await getReleases(github, dependency);
for (const release of releases) {
@@ -91,12 +127,6 @@ const main = async ({ github, releaseId, dependencies }) => {
}
}
const { data: release } = await github.rest.repos.getRelease({
owner,
repo,
release_id: releaseId,
});
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
let output = "";
+5
View File
@@ -11,6 +11,11 @@ async function main() {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
// matrix-js-sdk is built into ECMAScript modules. Make sure we declare it as such.
// See https://nodejs.org/api/packages.html#type.
pkgJson["type"] = "module";
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
+88 -23
View File
@@ -81,33 +81,37 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
};
}
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
beforeEach(
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});
// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});
await initCrypto(aliceClient);
});
await initCrypto(aliceClient);
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
afterEach(async () => {
await aliceClient.stopClient();
@@ -343,6 +347,67 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
expect(isCrossSigningReady).toBeTruthy();
});
it("should return false if identity is not trusted, even if the secrets are in 4S", async () => {
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
// Complete initial sync, to get the 4S account_data events stored
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// For this test we need to have a well-formed 4S setup.
const mockSecretInfo = {
encrypted: {
// Don't care about the actual values here, just need to be present for validation
KeyId: {
iv: "IVIVIVIVIVIVIV",
ciphertext: "CIPHERTEXTB64",
mac: "MACMACMAC",
},
},
};
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.secret_storage.key.KeyId",
content: {
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
// iv and mac not relevant for this test
},
},
{
type: "m.secret_storage.default_key",
content: {
key: "KeyId",
},
},
{
type: "m.cross_signing.master",
content: mockSecretInfo,
},
{
type: "m.cross_signing.user_signing",
content: mockSecretInfo,
},
{
type: "m.cross_signing.self_signing",
content: mockSecretInfo,
},
],
},
});
await aliceClient.startClient();
await syncPromise(aliceClient);
// Sanity: ensure that the secrets are in 4S
const status = await aliceClient.getCrypto()!.getCrossSigningStatus();
expect(status.privateKeysInSecretStorage).toBeTruthy();
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
});
describe("getCrossSigningKeyId", () => {
+351 -67
View File
@@ -19,13 +19,14 @@ import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { MockResponse, MockResponseFunction } from "fetch-mock";
import FetchMock from "fetch-mock";
import Olm from "@matrix-org/olm";
import * as testUtils from "../../test-utils/test-utils";
import {
advanceTimersUntil,
CRYPTO_BACKENDS,
emitPromise,
getSyncResponse,
InitCrypto,
mkEventCustom,
@@ -50,6 +51,7 @@ import {
ClientEvent,
createClient,
CryptoEvent,
HistoryVisibility,
IClaimOTKsResult,
IContent,
IDownloadKeyResult,
@@ -59,11 +61,11 @@ import {
MatrixClient,
MatrixEvent,
MatrixEventEvent,
MsgType,
PendingEventOrdering,
Room,
RoomMember,
RoomStateEvent,
HistoryVisibility,
} from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
@@ -80,12 +82,12 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
import {
CrossSigningKey,
CryptoCallbacks,
DecryptionFailureCode,
EventShieldColour,
EventShieldReason,
KeyBackupInfo,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { DecryptionError } from "../../../src/crypto/algorithms";
import { IKeyBackup } from "../../../src/crypto/backup";
import {
createOlmAccount,
@@ -99,12 +101,16 @@ import {
} from "./olm-utils";
import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
jest.useRealTimers();
});
/**
@@ -151,7 +157,7 @@ async function expectSendRoomKey(
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(
new RegExp("/sendToDevice/m.room.encrypted/"),
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
resolve(onSendRoomKey(content));
return {};
@@ -285,7 +291,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
* (or a function that returns one).
*/
function expectAliceKeyClaim(response: MockResponse | MockResponseFunction) {
function expectAliceKeyClaim(response: FetchMock.MockResponse | FetchMock.MockResponseFunction) {
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
}
@@ -464,56 +470,58 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("Unable to decrypt error codes", function () {
it("Encryption fails with expected UISI error", async () => {
beforeEach(() => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
it("Decryption fails with UISI error", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const awaitUISI = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
if (error.code == "MEGOLM_UNKNOWN_INBOUND_SESSION_ID") {
resolve();
}
});
});
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Ensure that the timestamp post-dates the creation of our device
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitUISI;
const ev = await awaitDecryption;
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
});
it("Encryption fails with expected Unknown Index error", async () => {
it("Decryption fails with Unknown Index error", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const awaitUnknownIndex = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
if (error.code == "OLM_UNKNOWN_MESSAGE_INDEX") {
resolve();
}
});
});
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
await aliceClient.getCrypto()!.importRoomKeys([testData.RATCHTED_MEGOLM_SESSION_DATA]);
// Alice gets both the events in a single sync
// Ensure that the timestamp post-dates the creation of our device
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
@@ -521,23 +529,236 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitUnknownIndex;
const ev = await awaitDecryption;
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
});
it("Encryption fails with Unable to decrypt for other errors", async () => {
describe("Historical events", () => {
async function sendEventAndAwaitDecryption(props: Partial<IEvent> = {}): Promise<MatrixEvent> {
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Ensure that the timestamp pre-dates the creation of our device: set it to 24 hours ago
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now() - 24 * 3600 * 1000,
...props,
};
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
return await awaitDecryption;
}
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_NO_BACKUP when there is no backup", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
});
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED when the backup is broken", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(
DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
);
});
newBackendOnly("fails with HISTORICAL_MESSAGE_WORKING_BACKUP when backup is working", async () => {
// The test backup data is signed by a dummy device. We'll need to tell Alice about the device, and
// later, tell her to trust it, so that she trusts the backup.
const e2eResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
e2eResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
await startClientAndAwaitFirstSync();
await aliceClient
.getCrypto()!
.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
// Tell Alice to trust the dummy device that signed the backup
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// Tell Alice to check and enable backup
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// Sanity: Alice should now have working backup.
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual(
testData.SIGNED_BACKUP_DATA.version,
);
// Finally! we can check what happens when we get an event.
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
});
newBackendOnly("fails with NOT_JOINED if user is not member of room", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "leave",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
});
newBackendOnly(
"fails with NOT_JOINED if user is not member of room (MSC4115 unstable prefix)",
async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.altName!]: "leave",
},
});
expect(ev.decryptionFailureReason).toEqual(
DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
);
},
);
newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports that the user
// was invited at the time the event was sent, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error,
// and instead get some other error, since the user should
// have gotten the key for the event.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "invite",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
newBackendOnly(
"fails with another error when the server reports user was a member of the room (MSC4115 unstable prefix)",
async () => {
// This tests that when the server reports that the user
// was invited at the time the event was sent, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error,
// and instead get some other error, since the user should
// have gotten the key for the event.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.altName!]: "invite",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports the user's
// membership, and reports that the user was joined, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and
// instead get some other error.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "join",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
newBackendOnly(
"fails with another error when the server reports user was a member of the room (MSC4115 unstable prefix)",
async () => {
// This tests that when the server reports the user's
// membership, and reports that the user was joined, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and
// instead get some other error.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.altName!]: "join",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
});
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
await aliceClient.getCrypto()!.importRoomKeys([testData.MEGOLM_SESSION_DATA]);
const awaitDecryptionError = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
aliceClient.on(MatrixEventEvent.Decrypted, (ev) => {
// rust and libolm can't have an exact 1:1 mapping for all errors,
// but some errors are part of API and should match
if (
error.code != "MEGOLM_UNKNOWN_INBOUND_SESSION_ID" &&
error.code != "OLM_UNKNOWN_MESSAGE_INDEX"
ev.decryptionFailureReason !== DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID &&
ev.decryptionFailureReason !== DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX
) {
resolve();
}
@@ -1007,10 +1228,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
return encryptedMessage;
}
afterEach(() => {
jest.useRealTimers();
});
newBackendOnly("should rotate the session after 2 messages", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1202,7 +1419,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.putOnce(
{ url: new RegExp("/send/"), name: "send-event" },
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
logger.log("/send:", content);
// make sure that a new session is used
@@ -1242,7 +1459,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
content: { algorithm: "m.megolm.v1.aes-sha2" },
}),
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
sender: aliceClient.getUserId()!,
}),
],
@@ -1267,7 +1484,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// mark the device as known, and resend.
aliceClient.setDeviceKnown(aliceClient.getUserId()!, "DEVICE_ID");
expectAliceKeyClaim((url: string, opts: RequestInit): MockResponse => {
expectAliceKeyClaim((url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519");
return getTestKeysClaimResponse(aliceClient.getUserId()!);
@@ -1379,7 +1596,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
expect(decryptedEvent.getContent().body).toEqual("42");
const exported = await aliceClient.exportRoomKeys();
const exported = await aliceClient.getCrypto()!.exportRoomKeysAsJson();
// start a new client
await aliceClient.stopClient();
@@ -1395,7 +1612,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
keyReceiver = new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
await initCrypto(aliceClient);
await aliceClient.importRoomKeys(exported);
await aliceClient.getCrypto()!.importRoomKeysAsJson(exported);
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1661,7 +1878,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
type: "m.room.member",
state_key: "@alice:localhost",
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -1810,7 +2027,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
type: "m.room.member",
state_key: "@alice:localhost",
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -1886,7 +2103,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
{
type: "m.room.member",
state_key: aliceClient.getUserId(),
content: { membership: "join" },
content: { membership: KnownMembership.Join },
event_id: "$alijoin",
},
],
@@ -1913,7 +2130,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
{
type: "m.room.member",
state_key: "@other:user",
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
event_id: "$otherinvite",
},
],
@@ -1927,7 +2144,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expectAliceKeyQuery({ device_keys: { "@other:user": {} }, failures: {} });
aliceClient.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => {
if (member.userId == "@other:user") {
aliceClient.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" });
aliceClient.sendMessage(testRoomId, { msgtype: MsgType.Text, body: "Hello, World" });
}
});
@@ -1963,11 +2180,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
const sendRoomMessageDefer = defer<MockResponse>();
const sendRoomMessageDefer = defer<FetchMock.MockResponse>();
const reqProm = new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/send/m.room.encrypted/"),
async (url: string, opts: RequestInit): Promise<MockResponse> => {
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
resolve(JSON.parse(opts.body as string));
return await sendRoomMessageDefer.promise;
},
@@ -2061,7 +2278,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.getOnce(new RegExp(membersPath), {
chunk: [
testUtils.mkMembershipCustom({
membership: "join",
membership: KnownMembership.Join,
sender: "@bob:xyz",
}),
],
@@ -2116,8 +2333,84 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("m.room_key.withheld handling", () => {
// TODO: there are a bunch more tests for this sort of thing in spec/unit/crypto/algorithms/megolm.spec.ts.
// They should be converted to integ tests and moved.
describe.each([
["m.blacklisted", "The sender has blocked you.", DecryptionFailureCode.MEGOLM_KEY_WITHHELD],
[
"m.unverified",
"The sender has disabled encrypting to unverified devices.",
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
],
])(
"Decryption fails with withheld error if a withheld notice with code '%s' is received",
(withheldCode, expectedMessage, expectedErrorCode) => {
it.each(["before", "after"])("%s the event", async (when) => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
let awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Send Alice an encrypted room event which looks like it was encrypted with a megolm session
async function sendEncryptedEvent() {
const event = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
const syncResponse = {
next_batch: 1,
rooms: { join: { [ROOM_ID]: { timeline: { events: [event] } } } },
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
}
// Send Alice a withheld notice
async function sendWithheldMessage() {
const withheldMessage = {
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: ROOM_ID,
sender_key: testData.ENCRYPTED_EVENT.content!.sender_key,
session_id: testData.ENCRYPTED_EVENT.content!.session_id,
code: withheldCode,
reason: "zzz",
},
};
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
to_device: { events: [withheldMessage] },
});
await syncPromise(aliceClient);
}
if (when === "before") {
await sendWithheldMessage();
await sendEncryptedEvent();
} else {
await sendEncryptedEvent();
// Make sure that the first attempt to decrypt has happened before the withheld arrives
await awaitDecryption;
awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
await sendWithheldMessage();
}
const ev = await awaitDecryption;
expect(ev.getContent()).toEqual({
body: `** Unable to decrypt: DecryptionError: ${expectedMessage} **`,
msgtype: "m.bad.encrypted",
});
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
});
},
);
oldBackendOnly("does not block decryption on an 'm.unavailable' report", async function () {
// there may be a key downloads for alice
@@ -2194,10 +2487,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
afterEach(() => {
jest.useRealTimers();
});
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
@@ -2260,10 +2549,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("getUserDeviceInfo", () => {
afterEach(() => {
jest.useRealTimers();
});
// From https://spec.matrix.org/v1.6/client-server-api/#post_matrixclientv3keysquery
// Using extracted response from matrix.org, it needs to have real keys etc to pass old crypto verification
const queryResponseBody = {
@@ -2752,10 +3037,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
afterEach(() => {
jest.useRealTimers();
});
it("Should be able to restore from 4S after bootstrap", async () => {
const backupVersion = "1";
await bootstrapSecurity(backupVersion);
@@ -3090,7 +3371,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const roomResponse = {
state: {
events: [
mkMembershipCustom({ membership: "join", sender: persistentStoreClient.getSafeUserId() }),
mkMembershipCustom({
membership: KnownMembership.Join,
sender: persistentStoreClient.getSafeUserId(),
}),
...stateEvents,
],
},
@@ -0,0 +1,181 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
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";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
cryptoCallbacks: {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
},
});
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
// count the number of times the dehydration key gets set
let setDehydrationCount = 0;
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
if (event.getType() === "org.matrix.msc3814") {
setDehydrationCount++;
}
});
const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;
// start dehydration -- we start with no dehydrated device, and we
// store the dehydrated device that we create
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
let dehydratedDeviceBody: any;
let dehydrationCount = 0;
let resolveDehydrationPromise: () => void;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
});
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
// a week later, we should have created another dehydrated device
const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve;
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationCount).toEqual(2);
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
// a new dehydration key will be set
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
});
const eventsResponse = jest.fn((url, opts) => {
// rehydrating should make two calls to the /events endpoint.
// The first time will return a single event, and the second
// time will return no events (which will signal to the
// rehydration function that it can stop)
const body = JSON.parse(opts.body as string);
const nextBatch = body.next_batch ?? "0";
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
return {
events,
next_batch: nextBatch + "1",
};
});
fetchMock.post(
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
eventsResponse,
);
await crypto.startDehydration(true);
expect(dehydrationCount).toEqual(3);
expect(setDehydrationCount).toEqual(2);
expect(eventsResponse.mock.calls).toHaveLength(2);
matrixClient.stopClient();
});
});
/** create a new secret storage and cross-signing keys */
async function initializeSecretStorage(
matrixClient: MatrixClient,
userId: string,
homeserverUrl: string,
): Promise<void> {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
const accountData: Map<string, object> = new Map();
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = accountData.get(name);
if (value) {
return value;
} else {
return {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
};
}
});
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = JSON.parse(opts.body as string);
accountData.set(name, value);
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
return {};
});
await matrixClient.initRustCrypto();
const crypto = matrixClient.getCrypto()! as RustCrypto;
// we need to process a sync so that the OlmMachine will upload keys
await crypto.preprocessToDeviceMessages([]);
await crypto.onSyncCompleted({});
// create initial secret storage
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true });
await matrixClient.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
}
+36 -23
View File
@@ -21,7 +21,7 @@ import { Mocked } from "jest-mock";
import {
createClient,
CryptoApi,
Crypto,
CryptoEvent,
ICreateClientOpts,
IEvent,
@@ -45,6 +45,7 @@ import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keyback
import { IKeyBackup } from "../../../src/crypto/backup";
import { flushPromises } from "../../test-utils/flushPromises";
import { defer, IDeferred } from "../../../src/utils";
import { DecryptionFailureCode } from "../../../src/crypto-api";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -192,28 +193,31 @@ 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);
beforeEach(
async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
// start after saving the private key
await aliceClient.startClient();
// start after saving the private key
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
});
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
} /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */,
10000,
);
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
@@ -239,8 +243,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
// 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,
);
// Eventually, decryption succeeds.
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
});
@@ -297,7 +310,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
describe("recover from backup", () => {
let aliceCrypto: CryptoApi;
let aliceCrypto: Crypto.CryptoApi;
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
@@ -783,7 +796,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
jest.runAllTimers();
jest.advanceTimersByTime(10 * 60 * 1000);
await failurePromise;
// Fix the endpoint to do successful uploads
@@ -816,7 +829,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
// run the timers, which will make the backup loop redo the request
await jest.runAllTimersAsync();
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
await successPromise;
await allKeysUploadedPromise;
});
+5 -4
View File
@@ -34,8 +34,9 @@ import { logger } from "../../../src/logger";
import * as testUtils from "../../test-utils/test-utils";
import { TestClient } from "../../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent, MsgType } from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { KnownMembership } from "../../../src/@types/membership";
let aliTestClient: TestClient;
const roomId = "!room:localhost";
@@ -216,7 +217,7 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
}
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
return client.sendMessage(roomId, { msgtype: MsgType.Text, body: "Hello, World" });
}
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
@@ -316,11 +317,11 @@ function firstSync(testClient: TestClient): Promise<void> {
state: {
events: [
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: bobUserId,
}),
],
+348 -63
View File
@@ -20,6 +20,10 @@ import fetchMock from "fetch-mock-jest";
import { createClient, CryptoEvent, IndexedDBCryptoStore } from "../../../src";
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
jest.setTimeout(15000);
@@ -62,18 +66,36 @@ describe("MatrixClient.initRustCrypto", () => {
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
});
it("should create the meta db if given a pickleKey", async () => {
it("should create the meta db if given a storageKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto();
await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) });
// should have two indexed dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
);
});
it("should create the meta db if given a storagePassword", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" });
// should have two indexed dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
@@ -93,69 +115,333 @@ describe("MatrixClient.initRustCrypto", () => {
await matrixClient.initRustCrypto();
});
it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
auth_data: {
public_key: "q+HZiJdHl2Yopv9GGvv7EYSzDMrAiRknK4glSdoaomI",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"reDp6Mu+j+tfUL3/T6f5OBT3N825Lzpc43vvG+RvjX6V+KxXzodBQArgCoeEHLtL9OgSBmNrhTkSOX87MWCKAw",
"ed25519:KMFSTJSMLB":
"F8tyV5W6wNi0GXTdSg+gxSCULQi0EYxdAAqfkyNq58KzssZMw5i+PRA0aI2b+D7NH/aZaJrtiYNHJ0gWLSQvAw",
describe("Libolm Migration", () => {
beforeEach(() => {
fetchMock.reset();
});
it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
const testStoreName = "test-store";
await populateStore(testStoreName, FULL_ACCOUNT_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: FULL_ACCOUNT_DATASET.userId,
deviceId: FULL_ACCOUNT_DATASET.deviceId,
cryptoStore,
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
});
const progressListener = jest.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getDeviceVerificationStatus(FULL_ACCOUNT_DATASET.userId, FULL_ACCOUNT_DATASET.deviceId);
// Check that the current device and identity trust is migrated correctly just after migration
expect(verificationStatus).toBeDefined();
expect(verificationStatus!.crossSigningVerified).toEqual(true);
expect(verificationStatus!.signedByOwner).toEqual(true);
// Do some basic checks on the imported data
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(
true,
);
// check the progress callback
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
// The first call should have progress == 0
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
expect(totalSteps).toBeGreaterThan(3000);
expect(firstProgress).toEqual(0);
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
const [progress, total] = progressListener.mock.calls[i];
expect(total).toEqual(totalSteps);
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
expect(progress).toBeLessThanOrEqual(totalSteps);
}
// The final call should have progress == total == -1
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
}, 60000);
describe("Private key backup migration", () => {
it("should not migrate the backup private key if backup has changed", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.newBackupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should not migrate the backup private key if backup has unknown algorithm", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
const backupResponse = {
...MSK_NOT_CACHED_DATASET.backupResponse,
algorithm: "m.megolm_backup.v8",
};
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should not migrate the backup private key if the backup has been deleted", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
},
},
version: "7",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "1",
count: 79,
});
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should migrate the backup private key if the backup matches", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeDefined();
});
});
const testStoreName = "test-store";
await populateStore(testStoreName);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
it("should not migrate if account data is missing", async () => {
// See https://github.com/element-hq/element-web/issues/27447
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@vdhtest200713:matrix.org",
deviceId: "KMFSTJSMLB",
cryptoStore,
pickleKey: "+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o",
// Given we have an almost-empty legacy account in the database
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No backup found" },
});
fetchMock.post("path:/_matrix/client/v3/keys/query", EMPTY_ACCOUNT_DATASET.keyQueryResponse);
const testStoreName = "test-store";
await populateStore(testStoreName, EMPTY_ACCOUNT_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: EMPTY_ACCOUNT_DATASET.userId,
deviceId: EMPTY_ACCOUNT_DATASET.deviceId,
cryptoStore,
pickleKey: EMPTY_ACCOUNT_DATASET.pickleKey,
});
// When we start Rust crypto, potentially triggering an upgrade
const progressListener = jest.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
// Then no error occurs, and no upgrade happens
expect(progressListener.mock.calls.length).toBe(0);
}, 60000);
describe("Legacy trust migration", () => {
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
const testStoreName = "test-store";
await populateStore(testStoreName, dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
await cryptoStore.startup();
return cryptoStore;
}
it("should not revert to untrusted if legacy was trusted but msk not in cache, big account", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(FULL_ACCOUNT_DATASET.dumpPath);
// Remove the master key from the cache
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
const objectStore = txn.objectStore("account");
objectStore.delete(`ssss_cache:master`);
});
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: FULL_ACCOUNT_DATASET.userId,
deviceId: FULL_ACCOUNT_DATASET.deviceId,
cryptoStore,
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus(FULL_ACCOUNT_DATASET.userId);
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
}, 60000);
it("should not revert to untrusted if legacy was trusted but msk not in cache", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus("@migration:localhost");
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
});
it("should not migrate local trust if key has changed", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.rotatedKeyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus("@migration:localhost");
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
});
it("should not migrate local trust if was not trusted in legacy", async () => {
// Just 404 here for the test
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/query", IDENTITY_NOT_TRUSTED_DATASET.keyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(IDENTITY_NOT_TRUSTED_DATASET.dumpPath);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: IDENTITY_NOT_TRUSTED_DATASET.userId,
deviceId: IDENTITY_NOT_TRUSTED_DATASET.deviceId,
cryptoStore,
pickleKey: IDENTITY_NOT_TRUSTED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus("@untrusted:localhost");
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
});
});
const progressListener = jest.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
// Do some basic checks on the imported data
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(true);
// check the progress callback
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
// The first call should have progress == 0
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
expect(totalSteps).toBeGreaterThan(3000);
expect(firstProgress).toEqual(0);
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
const [progress, total] = progressListener.mock.calls[i];
expect(total).toEqual(totalSteps);
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
expect(progress).toBeLessThanOrEqual(totalSteps);
}
// The final call should have progress == total == -1
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
}, 60000);
});
});
describe("MatrixClient.clearStores", () => {
@@ -164,10 +450,9 @@ describe("MatrixClient.clearStores", () => {
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
await matrixClient.initRustCrypto();
await matrixClient.initRustCrypto({ storagePassword: "testKey" });
expect(await indexedDB.databases()).toHaveLength(2);
await matrixClient.stopClient();
+3 -3
View File
@@ -17,7 +17,7 @@ limitations under the License.
import "fake-indexeddb/auto";
import anotherjson from "another-json";
import { MockResponse } from "fetch-mock";
import FetchMock from "fetch-mock";
import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
@@ -1511,7 +1511,7 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
return new Promise((resolve) => {
fetchMock.putOnce(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
resolve(JSON.parse(opts.body as string));
return {};
},
@@ -1535,7 +1535,7 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
+4 -3
View File
@@ -19,6 +19,7 @@ limitations under the License.
import { TestClient } from "../TestClient";
import * as testUtils from "../test-utils/test-utils";
import { logger } from "../../src/logger";
import { KnownMembership } from "../../src/@types/membership";
const ROOM_ID = "!room:id";
@@ -43,7 +44,7 @@ function getSyncResponse(roomMembers: string[]) {
stateEvents,
roomMembers.map((m) =>
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
sender: m,
}),
),
@@ -323,7 +324,7 @@ describe("DeviceList management:", function () {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
mship: KnownMembership.Leave,
sender: "@bob:xyz",
}),
],
@@ -357,7 +358,7 @@ describe("DeviceList management:", function () {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
mship: KnownMembership.Leave,
sender: "@bob:xyz",
}),
],
@@ -28,6 +28,7 @@ import {
} from "../../src";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient events", function () {
const selfUserId = "@alice:localhost";
@@ -85,7 +86,7 @@ describe("MatrixClient events", function () {
events: [
utils.mkMembership({
room: "!erufh:bar",
mship: "join",
mship: KnownMembership.Join,
user: "@foo:bar",
}),
utils.mkEvent({
@@ -272,7 +273,7 @@ describe("MatrixClient events", function () {
membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join");
expect(member.membership).toEqual(KnownMembership.Join);
});
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
newMemberInvokeCount++;
@@ -310,7 +311,7 @@ describe("MatrixClient events", function () {
});
client!.on(RoomMemberEvent.Membership, function (event, member) {
membershipInvokeCount++;
expect(member.membership).toEqual("join");
expect(member.membership).toEqual(KnownMembership.Join);
});
client!.startClient();
@@ -36,6 +36,7 @@ import { TestClient } from "../TestClient";
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
import { emitPromise } from "../test-utils/test-utils";
import { Feature, ServerSupport } from "../../src/feature";
import { KnownMembership } from "../../src/@types/membership";
const userId = "@alice:localhost";
const userName = "Alice";
@@ -63,7 +64,7 @@ const buildRelationPaginationQuery = (params: QueryDict): string => {
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userId,
name: userName,
event: false,
@@ -98,7 +99,7 @@ const INITIAL_SYNC_DATA = {
events: [
withoutRoomId(ROOM_NAME_EVENT),
utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
name: "Bob",
event: false,
@@ -607,11 +608,6 @@ describe("MatrixClient event timelines", function () {
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
@@ -1545,9 +1541,7 @@ describe("MatrixClient event timelines", function () {
expect(timelineSets).not.toBeNull();
respondToThreads(threadsResponse);
respondToThreads(threadsResponse);
respondToEvent(THREAD_ROOT);
respondToEvent(THREAD2_ROOT);
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
await flushHttp(room.fetchRoomThreads());
const threadIds = room.getThreads().map((thread) => thread.id);
@@ -1566,7 +1560,6 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.NewReply);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD2_ROOT);
await room.addLiveEvents([THREAD_REPLY2]);
await httpBackend.flushAllExpected();
await prom;
@@ -1643,7 +1636,7 @@ describe("MatrixClient event timelines", function () {
...THREAD_ROOT.unsigned!["m.relations"],
"io.element.thread": {
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
count: 2,
count: 1,
latest_event: THREAD_REPLY,
},
},
@@ -1692,7 +1685,6 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.Update);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD2_ROOT);
await room.addLiveEvents([THREAD_REPLY_REACTION]);
await httpBackend.flushAllExpected();
await prom;
@@ -1930,7 +1922,7 @@ describe("MatrixClient event timelines", function () {
// a state event, followed by a redaction thereof
const event = utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
});
const redaction = utils.mkEvent({
@@ -2007,11 +1999,6 @@ describe("MatrixClient event timelines", function () {
},
},
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
+212 -39
View File
@@ -19,7 +19,16 @@ import { Mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
import {
Filter,
JoinRule,
KnockRoomOpts,
MemoryStore,
Method,
Room,
RoomSummary,
SERVICE_TYPES,
} from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IFilterDefinition } from "../../src/filter";
@@ -27,6 +36,7 @@ import { ISearchResults } from "../../src/@types/search";
import { IStore } from "../../src/store";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { SetPresence } from "../../src/sync";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient", function () {
const userId = "@alice:localhost";
@@ -162,7 +172,7 @@ describe("MatrixClient", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
event: true,
}),
]);
@@ -182,7 +192,7 @@ describe("MatrixClient", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
event: true,
}),
]);
@@ -247,7 +257,7 @@ describe("MatrixClient", function () {
.when("POST", "/knock/" + encodeURIComponent(roomId))
.check((request) => {
expect(request.data).toEqual({ reason: opts.reason });
expect(request.queryParams).toEqual({ server_name: opts.viaServers });
expect(request.queryParams).toEqual({ server_name: opts.viaServers, via: opts.viaServers });
})
.respond(200, { room_id: roomId });
@@ -269,7 +279,7 @@ describe("MatrixClient", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "knock",
mship: KnownMembership.Knock,
event: true,
}),
]);
@@ -1283,18 +1293,109 @@ describe("MatrixClient", function () {
});
describe("getCapabilities", () => {
it("should cache by default", async () => {
it("should return cached capabilities if present", async () => {
const capsObject = {
"m.change_password": false,
};
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: {
"m.change_password": false,
},
capabilities: capsObject,
});
const prom = httpBackend.flushAllExpected();
const capabilities1 = await client.getCapabilities();
const capabilities2 = await client.getCapabilities();
client.startClient();
await httpBackend!.flushAllExpected();
expect(await client.getCapabilities()).toEqual(capsObject);
});
it("should fetch capabilities if cache not present", async () => {
const capsObject = {
"m.change_password": false,
};
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: capsObject,
});
const capsPromise = client.getCapabilities();
await httpBackend!.flushAllExpected();
expect(await capsPromise).toEqual(capsObject);
});
});
describe("getCachedCapabilities", () => {
it("should return cached capabilities or undefined", async () => {
const capsObject = {
"m.change_password": false,
};
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: capsObject,
});
expect(client.getCachedCapabilities()).toBeUndefined();
client.startClient();
await httpBackend!.flushAllExpected();
expect(client.getCachedCapabilities()).toEqual(capsObject);
});
});
describe("fetchCapabilities", () => {
const capsObject = {
"m.change_password": false,
};
beforeEach(() => {
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: capsObject,
});
});
afterEach(() => {
jest.useRealTimers();
});
it("should always fetch capabilities and then cache", async () => {
const prom = client.fetchCapabilities();
await httpBackend.flushAllExpected();
const caps = await prom;
expect(caps).toEqual(capsObject);
});
it("should write-through the cache", async () => {
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
client.startClient();
await httpBackend!.flushAllExpected();
expect(client.getCachedCapabilities()).toEqual(capsObject);
const newCapsObject = {
"m.change_password": true,
};
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: newCapsObject,
});
const prom = client.fetchCapabilities();
await httpBackend.flushAllExpected();
await prom;
expect(capabilities1).toStrictEqual(capabilities2);
expect(client.getCachedCapabilities()).toEqual(newCapsObject);
});
});
@@ -1709,6 +1810,102 @@ describe("MatrixClient", function () {
await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]);
});
});
describe("getRoomSummary", () => {
const roomId = "!foo:bar";
const encodedRoomId = encodeURIComponent(roomId);
const roomSummary: RoomSummary = {
"room_id": roomId,
"name": "My Room",
"avatar_url": "",
"topic": "My room topic",
"world_readable": false,
"guest_can_join": false,
"num_joined_members": 1,
"room_type": "",
"join_rule": JoinRule.Public,
"membership": "leave",
"im.nheko.summary.room_version": "6",
"im.nheko.summary.encryption": "algo",
};
const prefix = "/_matrix/client/unstable/im.nheko.summary/";
const suffix = `summary/${encodedRoomId}`;
const deprecatedSuffix = `rooms/${encodedRoomId}/summary`;
const errorUnrecogStatus = 404;
const errorUnrecogBody = {
errcode: "M_UNRECOGNIZED",
error: "Unsupported endpoint",
};
const errorBadreqStatus = 400;
const errorBadreqBody = {
errcode: "M_UNKNOWN",
error: "Invalid request",
};
it("should respond with a valid room summary object", () => {
httpBackend.when("GET", prefix + suffix).respond(200, roomSummary);
const prom = client.getRoomSummary(roomId).then((response) => {
expect(response).toEqual(roomSummary);
});
httpBackend.flush("");
return prom;
});
it("should allow fallback to the deprecated endpoint", () => {
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
httpBackend.when("GET", prefix + deprecatedSuffix).respond(200, roomSummary);
const prom = client.getRoomSummary(roomId).then((response) => {
expect(response).toEqual(roomSummary);
});
httpBackend.flush("");
return prom;
});
it("should respond to unsupported path with error", () => {
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
httpBackend.when("GET", prefix + deprecatedSuffix).respond(errorUnrecogStatus, errorUnrecogBody);
const prom = client.getRoomSummary(roomId).then(
function (response) {
throw Error("request not failed");
},
function (error) {
expect(error.httpStatus).toEqual(errorUnrecogStatus);
expect(error.errcode).toEqual(errorUnrecogBody.errcode);
expect(error.message).toEqual(`MatrixError: [${errorUnrecogStatus}] ${errorUnrecogBody.error}`);
},
);
httpBackend.flush("");
return prom;
});
it("should respond to invalid path arguments with error", () => {
httpBackend.when("GET", prefix).respond(errorBadreqStatus, errorBadreqBody);
const prom = client.getRoomSummary("notAroom").then(
function (response) {
throw Error("request not failed");
},
function (error) {
expect(error.httpStatus).toEqual(errorBadreqStatus);
expect(error.errcode).toEqual(errorBadreqBody.errcode);
expect(error.message).toEqual(`MatrixError: [${errorBadreqStatus}] ${errorBadreqBody.error}`);
},
);
httpBackend.flush("");
return prom;
});
});
});
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
@@ -1719,7 +1916,6 @@ function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
const buildEventMessageInThread = (root: MatrixEvent) =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1740,12 +1936,10 @@ const buildEventMessageInThread = (root: MatrixEvent) =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80098509 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventPollResponseReference = () =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1763,7 +1957,6 @@ const buildEventPollResponseReference = () =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80106237 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventReaction = (event: MatrixEvent) =>
@@ -1803,7 +1996,6 @@ const buildEventRedaction = (event: MatrixEvent) =>
const buildEventPollStartThreadRoot = () =>
new MatrixEvent({
age: 80108647,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "ENCRYPTEDSTUFF",
@@ -1817,12 +2009,10 @@ const buildEventPollStartThreadRoot = () =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80108647 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventReply = (target: MatrixEvent) =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1841,12 +2031,10 @@ const buildEventReply = (target: MatrixEvent) =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80098509 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventRoomName = () =>
new MatrixEvent({
age: 80123249,
content: {
name: "1 poll, 1 vote, 1 thread",
},
@@ -1857,12 +2045,10 @@ const buildEventRoomName = () =>
state_key: "",
type: "m.room.name",
unsigned: { age: 80123249 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventEncryption = () =>
new MatrixEvent({
age: 80123383,
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
@@ -1873,12 +2059,10 @@ const buildEventEncryption = () =>
state_key: "",
type: "m.room.encryption",
unsigned: { age: 80123383 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventGuestAccess = () =>
new MatrixEvent({
age: 80123473,
content: {
guest_access: "can_join",
},
@@ -1889,12 +2073,10 @@ const buildEventGuestAccess = () =>
state_key: "",
type: "m.room.guest_access",
unsigned: { age: 80123473 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventHistoryVisibility = () =>
new MatrixEvent({
age: 80123556,
content: {
history_visibility: "shared",
},
@@ -1905,14 +2087,12 @@ const buildEventHistoryVisibility = () =>
state_key: "",
type: "m.room.history_visibility",
unsigned: { age: 80123556 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventJoinRules = () =>
new MatrixEvent({
age: 80123696,
content: {
join_rule: "invite",
join_rule: KnownMembership.Invite,
},
event_id: "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
origin_server_ts: 1643815441191,
@@ -1921,12 +2101,10 @@ const buildEventJoinRules = () =>
state_key: "",
type: "m.room.join_rules",
unsigned: { age: 80123696 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventPowerLevels = () =>
new MatrixEvent({
age: 80124105,
content: {
ban: 50,
events: {
@@ -1957,16 +2135,14 @@ const buildEventPowerLevels = () =>
state_key: "",
type: "m.room.power_levels",
unsigned: { age: 80124105 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventMember = () =>
new MatrixEvent({
age: 80125279,
content: {
avatar_url: "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
displayname: "andybalaam-test1",
membership: "join",
membership: KnownMembership.Join,
},
event_id: "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
origin_server_ts: 1643815439608,
@@ -1975,12 +2151,10 @@ const buildEventMember = () =>
state_key: "@andybalaam-test1:matrix.org",
type: "m.room.member",
unsigned: { age: 80125279 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventCreate = () =>
new MatrixEvent({
age: 80126105,
content: {
room_version: "6",
},
@@ -1991,7 +2165,6 @@ const buildEventCreate = () =>
state_key: "",
type: "m.room.create",
unsigned: { age: 80126105 },
user_id: "@andybalaam-test1:matrix.org",
});
function assertObjectContains(obj: Record<string, any>, expected: any): void {
+3 -2
View File
@@ -6,6 +6,7 @@ import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient opts", function () {
const baseUrl = "http://localhost.or.something";
@@ -43,13 +44,13 @@ describe("MatrixClient opts", function () {
}),
utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userB,
name: "Bob",
}),
utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userId,
name: "Alice",
}),
+3 -3
View File
@@ -16,7 +16,7 @@ limitations under the License.
import HttpBackend from "matrix-mock-request";
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { EventStatus, MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
@@ -60,7 +60,7 @@ describe("MatrixClient retrying", function () {
// send a couple of events; the second will be queued
const p1 = client!
.sendMessage(roomId, {
msgtype: "m.text",
msgtype: MsgType.Text,
body: "m1",
})
.then(
@@ -77,7 +77,7 @@ describe("MatrixClient retrying", function () {
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client!.sendMessage(roomId, {
msgtype: "m.text",
msgtype: MsgType.Text,
body: "m2",
});
+14 -13
View File
@@ -30,6 +30,7 @@ import {
Room,
} from "../../src";
import { TestClient } from "../TestClient";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient room timelines", function () {
const userId = "@alice:localhost";
@@ -42,7 +43,7 @@ describe("MatrixClient room timelines", function () {
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userId,
name: userName,
});
@@ -76,7 +77,7 @@ describe("MatrixClient room timelines", function () {
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
name: "Bob",
}),
@@ -316,7 +317,7 @@ describe("MatrixClient room timelines", function () {
// make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: userId,
room: roomId,
name: "Old Alice",
@@ -326,16 +327,16 @@ describe("MatrixClient room timelines", function () {
// make an m.room.member event with prev_content for alice's nick
// change
const oldMshipEvent = utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: userId,
room: roomId,
name: userName,
url: "mxc://some/url",
});
oldMshipEvent.prev_content = {
oldMshipEvent.unsigned!.prev_content = {
displayname: "Old Alice",
avatar_url: undefined,
membership: "join",
membership: KnownMembership.Join,
};
// set the list of events to return on scrollback (/messages)
@@ -487,7 +488,7 @@ describe("MatrixClient room timelines", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
name: "New Name",
}),
utils.mkMessage({ user: userId, room: roomId }),
@@ -554,13 +555,13 @@ describe("MatrixClient room timelines", function () {
utils.mkMembership({
user: userC,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
name: "C",
}),
utils.mkMembership({
user: userC,
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
skey: userD,
}),
];
@@ -571,9 +572,9 @@ describe("MatrixClient room timelines", function () {
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC)!.name).toEqual("C");
expect(room.currentState.getMember(userC)!.membership).toEqual("join");
expect(room.currentState.getMember(userC)!.membership).toEqual(KnownMembership.Join);
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
expect(room.currentState.getMember(userD)!.membership).toEqual("invite");
expect(room.currentState.getMember(userD)!.membership).toEqual(KnownMembership.Invite);
});
});
});
@@ -598,9 +599,9 @@ describe("MatrixClient room timelines", function () {
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
expect(room.currentState.getMember(userId)!.membership).toEqual("join");
expect(room.currentState.getMember(userId)!.membership).toEqual(KnownMembership.Join);
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual("join");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(KnownMembership.Join);
});
});
});
@@ -105,13 +105,13 @@ describe("MatrixClient syncing errors", () => {
await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[1].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
});
@@ -119,6 +119,7 @@ describe("MatrixClient syncing errors", () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock
.get("end:capabilities", {})
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
+530 -144
View File
@@ -39,6 +39,7 @@ import {
IndexedDBStore,
RelationType,
EventType,
MatrixEventEvent,
} from "../../src";
import { ReceiptType } from "../../src/@types/read_receipts";
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
@@ -46,6 +47,8 @@ import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost";
@@ -123,7 +126,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -151,10 +154,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "invite",
membership: KnownMembership.Invite,
},
// XXX: And other fields required on an event
},
@@ -167,10 +170,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "invite",
membership: KnownMembership.Invite,
},
// XXX: And other fields required on an event
},
@@ -193,22 +196,22 @@ describe("MatrixClient syncing", () => {
// Room, string, string
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("invite");
expect(membership).toBe(KnownMembership.Invite);
expect(oldMembership).toBeFalsy();
// Second fire: a leave
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("leave");
expect(oldMembership).toBe("invite");
expect(membership).toBe(KnownMembership.Leave);
expect(oldMembership).toBe(KnownMembership.Invite);
// Third/final fire: a second invite
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("invite");
expect(oldMembership).toBe("leave");
expect(membership).toBe(KnownMembership.Invite);
expect(oldMembership).toBe(KnownMembership.Leave);
});
});
@@ -238,7 +241,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "knock",
membership: KnownMembership.Knock,
},
},
],
@@ -266,10 +269,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "knock",
membership: KnownMembership.Knock,
},
// XXX: And other fields required on an event
},
@@ -282,10 +285,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "knock",
membership: KnownMembership.Knock,
},
// XXX: And other fields required on an event
},
@@ -308,22 +311,22 @@ describe("MatrixClient syncing", () => {
// Room, string, string
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("knock");
expect(membership).toBe(KnownMembership.Knock);
expect(oldMembership).toBeFalsy();
// Second fire: a leave
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("leave");
expect(oldMembership).toBe("knock");
expect(membership).toBe(KnownMembership.Leave);
expect(oldMembership).toBe(KnownMembership.Knock);
// Third/final fire: a second knock
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("knock");
expect(oldMembership).toBe("leave");
expect(membership).toBe(KnownMembership.Knock);
expect(oldMembership).toBe(KnownMembership.Leave);
});
});
@@ -381,7 +384,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -421,7 +424,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "knock",
membership: KnownMembership.Knock,
},
},
],
@@ -533,12 +536,12 @@ describe("MatrixClient syncing", () => {
events: [
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -556,7 +559,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -589,7 +592,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -617,7 +620,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -644,7 +647,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -719,12 +722,12 @@ describe("MatrixClient syncing", () => {
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -750,13 +753,13 @@ describe("MatrixClient syncing", () => {
events: [
utils.mkMembership({
room: roomTwo,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -1247,7 +1250,7 @@ describe("MatrixClient syncing", () => {
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: userA,
});
@@ -1508,12 +1511,12 @@ describe("MatrixClient syncing", () => {
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -1605,12 +1608,12 @@ describe("MatrixClient syncing", () => {
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -1645,6 +1648,99 @@ describe("MatrixClient syncing", () => {
});
});
it("should zero total notifications for threads when absent from the notifications object", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
});
it("should zero highlight notifications for threads in encrypted rooms", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 0,
notification_count: 0,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
});
it("should not zero highlight notifications for threads in encrypted rooms", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 0,
notification_count: 0,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
});
it("caches unknown threads receipts and replay them when the thread is created", async () => {
const THREAD_ID = "$unknownthread:localhost";
@@ -1732,64 +1828,351 @@ describe("MatrixClient syncing", () => {
});
});
it("should apply encrypted notification logic for events within the same sync blob", async () => {
const roomId = "!room123:server";
const syncData = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkEvent({
room: roomId,
event: true,
skey: "",
type: EventType.RoomEncryption,
content: {},
}),
utils.mkMessage({
room: roomId,
user: otherUserId,
msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomId,
mship: "join",
user: otherUserId,
}),
utils.mkMembership({
room: roomId,
mship: "join",
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomId,
user: selfUserId,
content: {},
}),
],
describe("encrypted notification logic", () => {
let roomId: string;
let syncData: ISyncResponse;
beforeEach(() => {
roomId = "!room123:server";
syncData = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkEvent({
room: roomId,
event: true,
skey: "",
type: EventType.RoomEncryption,
content: {},
}),
utils.mkMessage({
room: roomId,
user: otherUserId,
msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomId,
user: selfUserId,
content: {},
}),
],
},
},
},
},
},
} as unknown as ISyncResponse;
} as unknown as ISyncResponse;
});
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
it("should apply encrypted notification logic for events within the same sync blob", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
});
it("should recalculate highlights on unthreaded receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// add a receipt for the first event in the room (let's say the user has already read that one)
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1 },
},
},
},
type: "m.receipt",
},
];
// Now add a highlighting event after that receipt
const pingEvent = utils.mkMessage({
room: roomId,
user: otherUserId,
msg: client?.getUserId() + " ping",
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
// fudge this to make it a highlight
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if (ev.getId() === pingEvent.event_id) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// the room should now have one highlight since our receipt was before the ping message
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("should recalculate highlights on main thread receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// add a receipt for the first event in the room (let's say the user has already read that one)
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: "main" },
},
},
},
type: "m.receipt",
},
];
// Now add a highlighting event after that receipt
const pingEvent = utils.mkMessage({
room: roomId,
user: otherUserId,
msg: client?.getUserId() + " ping",
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
// fudge this to make it a highlight
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if (ev.getId() === pingEvent.event_id) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// the room should now have one highlight since our receipt was before the ping message
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
describe("notification processing in threads", () => {
let threadEvent1: IRoomEvent;
let threadEvent2: IRoomEvent;
let firstEventId: string;
beforeEach(() => {
firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// Add a threaded event off of the first event
threadEvent1 = utils.mkEvent({
type: EventType.RoomMessage,
user: otherUserId,
room: roomId,
ts: 500,
content: {
"body": "first thread response",
"m.relates_to": {
"event_id": firstEventId,
"m.in_reply_to": {
event_id: firstEventId,
},
"rel_type": "io.element.thread",
},
},
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(threadEvent1);
// ...and another
threadEvent2 = utils.mkEvent({
type: EventType.RoomMessage,
user: otherUserId,
room: roomId,
ts: 1500,
content: {
"body": "second thread response",
"m.relates_to": {
"event_id": firstEventId,
"m.in_reply_to": {
event_id: firstEventId,
},
"rel_type": "io.element.thread",
},
},
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(threadEvent2);
// fudge to make these highlights
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
});
it("checks threads with notifications on unthreaded receipts", async () => {
const myUserId = client!.getUserId()!;
// add a receipt for a random, ficticious thread, otherwise the client will
// think that the thread is before any threaded receipts and ignore it.
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: "some_other_thread" },
},
},
},
type: "m.receipt",
},
];
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient({ threadSupport: true });
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
// pretend that the client has decrypted an event to trigger it to compute
// local notifications
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent1.event_id)!);
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent2.event_id)!);
expect(room).toBeInstanceOf(Room);
// we should now have one highlight: the unread message that pings
expect(
room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight),
).toEqual(2);
const syncData2 = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1 },
},
},
},
type: "m.receipt",
},
],
},
},
},
},
} as unknown as ISyncResponse;
httpBackend!.when("GET", "/sync").respond(200, syncData2);
await Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
});
it("should recalculate highlights on threaded receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
// add a receipt for the first message in the threadm leaving the second one unread
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[threadEvent1.event_id]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: firstEventId },
},
},
},
type: "m.receipt",
},
];
// fudge to make both thread replies highlights
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient({ threadSupport: true });
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// pretend that the client has decrypted an event to trigger it to compute
// local notifications
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
// the room should now have one highlight: the second thread message
expect(room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight)).toBe(
1,
);
});
});
});
});
@@ -1898,67 +2281,70 @@ describe("MatrixClient syncing", () => {
httpBackend!.expectedRequests = [];
});
it("should return a room based on the room initialSync API", async () => {
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
room_id: roomOne,
membership: "leave",
messages: {
start: "start",
end: "end",
chunk: [
it.each([undefined, 100])(
"should return a room based on the room initialSync API with limit %s",
async (limit) => {
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
room_id: roomOne,
membership: KnownMembership.Leave,
messages: {
start: "start",
end: "end",
chunk: [
{
content: { body: "Message 1" },
type: "m.room.message",
event_id: "$eventId1",
sender: userA,
origin_server_ts: 12313525,
room_id: roomOne,
},
{
content: { body: "Message 2" },
type: "m.room.message",
event_id: "$eventId2",
sender: userB,
origin_server_ts: 12315625,
room_id: roomOne,
},
],
},
state: [
{
content: { body: "Message 1" },
type: "m.room.message",
event_id: "$eventId1",
content: { name: "Room Name" },
type: "m.room.name",
event_id: "$eventId",
sender: userA,
origin_server_ts: 12313525,
room_id: roomOne,
},
{
content: { body: "Message 2" },
type: "m.room.message",
event_id: "$eventId2",
sender: userB,
origin_server_ts: 12315625,
origin_server_ts: 12314525,
state_key: "",
room_id: roomOne,
},
],
},
state: [
{
content: { name: "Room Name" },
type: "m.room.name",
event_id: "$eventId",
sender: userA,
origin_server_ts: 12314525,
state_key: "",
room_id: roomOne,
},
],
presence: [
{
content: {},
type: "m.presence",
sender: userA,
},
],
});
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
presence: [
{
content: {},
type: "m.presence",
sender: userA,
},
],
});
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
const prom = client!.peekInRoom(roomOne);
await httpBackend!.flushAllExpected();
const room = await prom;
const prom = client!.peekInRoom(roomOne, limit);
await httpBackend!.flushAllExpected();
const room = await prom;
expect(room.roomId).toBe(roomOne);
expect(room.getMyMembership()).toBe("leave");
expect(room.name).toBe("Room Name");
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
expect(room.timeline[0].getContent().body).toBe("Message 1");
expect(room.timeline[1].getContent().body).toBe("Message 2");
client?.stopPeeking();
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
await httpBackend!.flushAllExpected();
});
expect(room.roomId).toBe(roomOne);
expect(room.getMyMembership()).toBe(KnownMembership.Leave);
expect(room.name).toBe("Room Name");
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
expect(room.timeline[0].getContent().body).toBe("Message 1");
expect(room.timeline[1].getContent().body).toBe("Message 2");
client?.stopPeeking();
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
await httpBackend!.flushAllExpected();
},
);
});
describe("user account data", () => {
@@ -2042,7 +2428,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -34,6 +34,7 @@ import { TestClient } from "../TestClient";
import { ReceiptType } from "../../src/@types/read_receipts";
import { mkThread } from "../test-utils/thread";
import { SyncState } from "../../src/sync";
import { KnownMembership } from "../../src/@types/membership";
const userA = "@alice:localhost";
const userB = "@bob:localhost";
@@ -151,7 +152,7 @@ describe("MatrixClient syncing", () => {
await client!.sendEvent(roomId, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: threadReply.getId(),
event_id: threadReply.getId()!,
key: "",
},
});
@@ -229,7 +230,7 @@ describe("MatrixClient syncing", () => {
content: {
avatar_url: "",
displayname: userB,
membership: "join",
membership: KnownMembership.Join,
},
origin_server_ts: 2,
sender: userB,
@@ -270,7 +271,7 @@ describe("MatrixClient syncing", () => {
},
{
content: {
join_rule: "invite",
join_rule: KnownMembership.Invite,
},
origin_server_ts: 4,
sender: userB,
@@ -316,7 +317,7 @@ describe("MatrixClient syncing", () => {
avatar_url: "",
displayname: userA,
is_direct: true,
membership: "invite",
membership: KnownMembership.Invite,
},
origin_server_ts: 8,
sender: userB,
@@ -338,7 +339,7 @@ describe("MatrixClient syncing", () => {
content: {
avatar_url: "",
displayname: userA,
membership: "join",
membership: KnownMembership.Join,
},
origin_server_ts: 10,
sender: userA,
@@ -0,0 +1,358 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
import { mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import {
MSC4108FailureReason,
MSC4108RendezvousSession,
MSC4108SecureChannel,
MSC4108SignInWithQR,
PayloadType,
RendezvousError,
} from "../../../src/rendezvous";
import { defer } from "../../../src/utils";
import {
ClientPrefix,
DEVICE_CODE_SCOPE,
IHttpOpts,
IMyDevice,
MatrixClient,
MatrixError,
MatrixHttpApi,
} from "../../../src";
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
const baseUrl = "https://example.com";
const crypto = {
exportSecretsForQrLogin: jest.fn(),
};
const client = {
doesServerSupportUnstableFeature(feature: string) {
return Promise.resolve(opts.msc4108Enabled && feature === "org.matrix.msc4108");
},
getUserId() {
return opts.userId;
},
getDeviceId() {
return opts.deviceId;
},
baseUrl,
getDomain: () => "example.com",
getDevice: jest.fn(),
getCrypto: jest.fn(() => crypto),
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
} as unknown as MatrixClient;
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
baseUrl: client.baseUrl,
prefix: ClientPrefix.Unstable,
onlyData: true,
});
return client;
}
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: {
"Content-Type": "application/json",
},
keys: [],
});
});
afterEach(() => {
fetchMock.reset();
});
const url = "https://fallbackserver/rz/123";
const deviceId = "DEADB33F";
const verificationUri = "https://example.com/verify";
const verificationUriComplete = "https://example.com/verify/complete";
it("should generate qr code data as expected", async () => {
const session = new MSC4108RendezvousSession({
url,
});
const channel = new MSC4108SecureChannel(session);
const login = new MSC4108SignInWithQR(channel, false);
await login.generateCode();
const code = login.code;
expect(code).toHaveLength(71);
const text = new TextDecoder().decode(code);
expect(text.startsWith("MATRIX")).toBeTruthy();
expect(text.endsWith(url)).toBeTruthy();
// Assert that the code is stable
await login.generateCode();
expect(login.code).toEqual(code);
});
describe("should be able to connect as a reciprocating device", () => {
let client: MatrixClient;
let ourLogin: MSC4108SignInWithQR;
let opponentLogin: MSC4108SignInWithQR;
beforeEach(async () => {
let ourData = defer<string>();
let opponentData = defer<string>();
const ourMockSession = {
send: jest.fn(async (newData) => {
ourData.resolve(newData);
}),
receive: jest.fn(() => {
const prom = opponentData.promise;
prom.then(() => {
opponentData = defer();
});
return prom;
}),
url,
cancelled: false,
cancel: () => {
// @ts-ignore
ourMockSession.cancelled = true;
ourData.resolve("");
},
} as unknown as MSC4108RendezvousSession;
const opponentMockSession = {
send: jest.fn(async (newData) => {
opponentData.resolve(newData);
}),
receive: jest.fn(() => {
const prom = ourData.promise;
prom.then(() => {
ourData = defer();
});
return prom;
}),
url,
} as unknown as MSC4108RendezvousSession;
client = makeMockClient({ userId: "@alice:example.com", deviceId: "alice", msc4108Enabled: true });
const ourChannel = new MSC4108SecureChannel(ourMockSession);
const qrCodeData = QrCodeData.fromBytes(
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
);
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
ourLogin = new MSC4108SignInWithQR(ourChannel, true, client);
opponentLogin = new MSC4108SignInWithQR(opponentChannel, false);
});
it("should be able to connect with opponent and share server name & check code", async () => {
await Promise.all([
expect(ourLogin.negotiateProtocols()).resolves.toEqual({}),
expect(opponentLogin.negotiateProtocols()).resolves.toEqual({ serverName: client.getDomain() }),
]);
expect(ourLogin.checkCode).toBe(opponentLogin.checkCode);
});
it("should be able to connect with opponent and share verificationUri", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
verificationUri: verificationUriComplete,
}),
// We don't have the new device side of this flow implemented at this time so mock it
// @ts-ignore
opponentLogin.send({
type: PayloadType.Protocol,
protocol: "device_authorization_grant",
device_authorization_grant: {
verification_uri: verificationUri,
verification_uri_complete: verificationUriComplete,
},
device_id: deviceId,
}),
]);
});
it("should abort if device already exists", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
// We don't have the new device side of this flow implemented at this time so mock it
// @ts-ignore
opponentLogin.send({
type: PayloadType.Protocol,
protocol: "device_authorization_grant",
device_authorization_grant: {
verification_uri: verificationUri,
},
device_id: deviceId,
}),
]);
});
it("should abort on unsupported protocol", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow(
"Received a request for an unsupported protocol",
),
// We don't have the new device side of this flow implemented at this time so mock it
// @ts-ignore
opponentLogin.send({
type: PayloadType.Protocol,
protocol: "device_authorization_grant_v2",
device_authorization_grant: {
verification_uri: verificationUri,
},
device_id: deviceId,
}),
]);
});
it("should be able to connect with opponent and share secrets", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
// We don't have the new device side of this flow implemented at this time so mock it
// @ts-ignore
ourLogin.expectingNewDeviceId = "DEADB33F";
const ourProm = ourLogin.shareSecrets();
// Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here
// @ts-ignore
await opponentLogin.receive();
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
const payload = {
secrets: expect.objectContaining(secrets),
};
await Promise.all([
expect(ourProm).resolves.toEqual(payload),
expect(opponentLogin.shareSecrets()).resolves.toEqual(payload),
]);
});
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
// TODO: mock timers properly
return -1 as any;
});
jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
// We don't have the new device side of this flow implemented at this time so mock it
// @ts-ignore
ourLogin.expectingNewDeviceId = "DEADB33F";
// @ts-ignore
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
const ourProm = ourLogin.shareSecrets();
await expect(ourProm).rejects.toThrow("New device not found");
});
it("should abort on unexpected errors", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
// We don't have the new device side of this flow implemented at this time so mock it
// @ts-ignore
ourLogin.expectingNewDeviceId = "DEADB33F";
// @ts-ignore
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
);
await expect(ourLogin.shareSecrets()).rejects.toThrow("The message");
});
it("should abort on declined login", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
await ourLogin.declineLoginOnExistingDevice();
await expect(opponentLogin.shareSecrets()).rejects.toThrow(
new RendezvousError("Failed", MSC4108FailureReason.UserCancelled),
);
});
it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
// TODO: mock timers properly
return -1 as any;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
// We don't have the new device side of this flow implemented at this time so mock it
// @ts-ignore
ourLogin.expectingNewDeviceId = "DEADB33F";
const ourProm = ourLogin.shareSecrets();
const opponentProm = opponentLogin.shareSecrets();
// Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here
// @ts-ignore
await opponentLogin.receive();
const deferred = defer<IMyDevice>();
mocked(client.getDevice).mockReturnValue(deferred.promise);
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
deferred.resolve({} as IMyDevice);
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
await Promise.all([
expect(ourProm).rejects.toThrow("User cancelled"),
expect(opponentProm).rejects.toThrow("Unexpected message received"),
]);
});
});
});
+20 -19
View File
@@ -43,6 +43,7 @@ import { IStoredClientOpts } from "../../src";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership";
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
@@ -189,7 +190,7 @@ describe("SlidingSyncSdk", () => {
name: "A",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
],
@@ -204,7 +205,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
@@ -216,7 +217,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
mkOwnEvent(EventType.RoomMessage, { body: "world C" }),
@@ -229,7 +230,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
mkOwnEvent(EventType.RoomMessage, { body: "world D" }),
@@ -244,7 +245,7 @@ describe("SlidingSyncSdk", () => {
invite_state: [
{
type: EventType.RoomMember,
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
state_key: selfUserId,
sender: "@bob:localhost",
event_id: "$room_e_invite",
@@ -265,7 +266,7 @@ describe("SlidingSyncSdk", () => {
name: "#foo:localhost",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "This should be ignored" }, ""),
@@ -281,7 +282,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
joined_count: 5,
@@ -293,7 +294,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
],
@@ -308,7 +309,7 @@ describe("SlidingSyncSdk", () => {
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomA].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
});
@@ -318,7 +319,7 @@ describe("SlidingSyncSdk", () => {
const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomB].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
});
@@ -372,7 +373,7 @@ describe("SlidingSyncSdk", () => {
const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomH].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
// check the entire timeline is correct
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
@@ -383,7 +384,7 @@ describe("SlidingSyncSdk", () => {
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getMyMembership()).toEqual("invite");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Invite);
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
@@ -603,9 +604,9 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
],
});
await httpBackend!.flush("/profile", 1, 1000);
@@ -719,7 +720,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
@@ -923,7 +924,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
@@ -964,7 +965,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
@@ -1050,12 +1051,12 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
{
type: EventType.RoomMember,
state_key: alice,
content: { membership: "join" },
content: { membership: KnownMembership.Join },
sender: alice,
origin_server_ts: Date.now(),
event_id: "$alice",
+5 -5
View File
@@ -107,8 +107,8 @@ describe("SlidingSync", () => {
onRequest: (initial) => {
return { initial: initial };
},
onResponse: (res) => {
return {};
onResponse: async (res) => {
return;
},
when: () => ExtensionState.PreProcess,
};
@@ -1572,7 +1572,7 @@ describe("SlidingSync", () => {
onPreExtensionRequest = () => {
return extReq;
};
onPreExtensionResponse = (resp) => {
onPreExtensionResponse = async (resp) => {
extensionOnResponseCalled = true;
callbackOrder.push("onPreExtensionResponse");
expect(resp).toEqual(extResp);
@@ -1613,7 +1613,7 @@ describe("SlidingSync", () => {
return undefined;
};
let responseCalled = false;
onPreExtensionResponse = (resp) => {
onPreExtensionResponse = async (resp) => {
responseCalled = true;
};
httpBackend!
@@ -1649,7 +1649,7 @@ describe("SlidingSync", () => {
};
let responseCalled = false;
const callbackOrder: string[] = [];
onPostExtensionResponse = (resp) => {
onPostExtensionResponse = async (resp) => {
expect(resp).toEqual(extResp);
responseCalled = true;
callbackOrder.push("onPostExtensionResponse");
-4
View File
@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
global.DOMException = DOMException as typeof global.DOMException;
jest.mock("../src/http-api/utils", () => ({
...jest.requireActual("../src/http-api/utils"),
// We mock timeoutSignal otherwise it causes tests to leave timers running
+4 -2
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import { MockOptionsMethodPut } from "fetch-mock";
import { ISyncResponder } from "./SyncResponder";
@@ -40,7 +39,10 @@ export class AccountDataAccumulator {
* @param opts - options to pass to fetchMock
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public interceptSetAccountData(accountDataType: string, opts?: MockOptionsMethodPut): Promise<any> {
public interceptSetAccountData(
accountDataType: string,
opts?: Parameters<(typeof fetchMock)["put"]>[2],
): Promise<any> {
return new Promise((resolve) => {
// Called when the cross signing key is uploaded
fetchMock.put(
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
import debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";
import FetchMock from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients
*
@@ -80,7 +80,7 @@ export class SyncResponder implements ISyncResponder {
);
}
private async onSyncRequest(): Promise<MockResponse> {
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
switch (this.state) {
case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready");
+1 -1
View File
@@ -88,6 +88,6 @@ export const mockClientMethodsEvents = () => ({
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockReturnValue({}),
getCachedCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
});
+8 -6
View File
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OidcClientConfig } from "../../src";
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
/**
* Makes a valid OidcClientConfig with minimum valid values
@@ -26,8 +25,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
const metadata = mockOpenIdConfiguration(issuer);
return {
issuer,
account: issuer + "account",
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
@@ -40,14 +38,18 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): 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"],
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
code_challenge_methods_supported: ["S256"],
});
+8 -5
View File
@@ -19,6 +19,7 @@ import {
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
import { TEST_ROOM_ID } from "./test-data";
import { KnownMembership, Membership } from "../../src/@types/membership";
/**
* Return a promise that is resolved when the client next emits a
@@ -87,7 +88,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
mkMembershipCustom({
membership: "join",
membership: KnownMembership.Join,
sender: roomMembers[i],
}),
);
@@ -172,8 +173,10 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
prev_content: opts.prev_content,
unsigned: opts.unsigned || {},
unsigned: {
...opts.unsigned,
prev_content: opts.prev_content,
},
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
redacts: opts.redacts,
@@ -251,7 +254,7 @@ export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<I
interface IMembershipOpts {
room?: string;
mship: string;
mship: Membership;
sender?: string;
user?: string;
skey?: string;
@@ -297,7 +300,7 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
}
export function mkMembershipCustom<T>(
base: T & { membership: string; sender: string; content?: IContent },
base: T & { membership: Membership; sender: string; content?: IContent },
): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
const content = base.content || {};
return mkEventCustom({
@@ -1,11 +1,13 @@
## Dump of libolm indexeddb cryptostore
## Dumps of libolm indexeddb cryptostore
This directory contains a dump of a real indexeddb store from a session using
This directory contains several dumps of real indexeddb stores from a session using
libolm crypto.
The corresponding pickle key is `+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o`.
Each directory contains, in dump.json, a dump of data created by pasting the following
code into the browser console; and in index.ts, details of the user, pickle key,
and corresponding key query and backup responses (`DumpDataSetInfo`).
It was created by pasting the following into the browser console:
The dump is created by pasting the following into the browser console:
```javascript
async function exportIndexedDb(name) {
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
## Dump of an empty libolm indexeddb cryptostore to test skipping migration
A dump of an account which is almost completely empty, and totally unsuitable
for use as a real account.
This dump was manually created by copying and editing full_account.
Created to test
["Unable to restore session" error due due to half-initialised legacy indexeddb crypto store #27447](https://github.com/element-hq/element-web/issues/27447).
We should not launch the Rust migration code when we find a DB in this state.
@@ -0,0 +1,14 @@
{
"account": [],
"device_data": [],
"inbound_group_sessions": [],
"inbound_group_sessions_withheld": [],
"notified_error_devices": [],
"outgoingRoomKeyRequests": [],
"parked_shared_history": [],
"rooms": [],
"session_problems": [],
"sessions": [],
"sessions_needing_backup": [],
"shared_history_inbound_group_sessions": []
}
@@ -0,0 +1,35 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEYS_QUERY_RESPONSE = { device_keys: { "@emptyuser:example.com": {} } };
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const EMPTY_ACCOUNT_DATASET: DumpDataSetInfo = {
userId: "@emptyuser:example.com",
deviceId: "EMPTYDEVIC",
pickleKey: "+/bcdefghijklmnopqrstu1/zyxvutsrqponmlkjih2",
keyQueryResponse: KEYS_QUERY_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/empty_account/dump.json",
};
@@ -0,0 +1,4 @@
## Dump of a libolm indexeddb cryptostore to test migration of a full account
A dump of an account containing a complete set of data to migrate.
The data set is substantial enough to allow for testing of chunking mechanisms and progress reporting during the migration process.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEYS_QUERY_RESPONSE: any = {
device_keys: {
"@vdhtest200713:matrix.org": {
KMFSTJSMLB: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "KMFSTJSMLB",
keys: {
"curve25519:KMFSTJSMLB": "LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU",
"ed25519:KMFSTJSMLB": "qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw",
},
user_id: "@vdhtest200713:matrix.org",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:KMFSTJSMLB":
"aE+PdxLAdwQ/xfJwLmqebvt/lrT97fZas2SQFFrM+dPmHxQtjyS8csm88BLfGRjJKK1B/vWev3AaKqQZwLTUAw",
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
"lCd4SA/JT1nnxsgN9yQaLJQhH5hkLMVVx6ba5JAjL1wpWVqyPxzMJHImX6vTztk6S8rybcdfYkea5W/Ii+4HCQ",
},
},
},
},
},
master_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["master"],
keys: {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI": "gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:MWOGVUTXZN":
"stOu1aHbhsWB/Aj5M/HqBR83QzME+682C995Uc8JxSmmyrlWmgG8QrnoUDG2OFR1t6zNQ+QLEilU4WNEOV73DQ",
},
},
},
},
self_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["self_signing"],
keys: {
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k": "lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"HKTC7NoBhAkfJtmemmkn/HvCCgBQViWZ0uH7aGPRaWMDFgD8T7Q+y1j3FKZv4mhSopR85Fq3FRyXsG8OVvGeBA",
},
},
},
},
user_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["user_signing"],
keys: {
"ed25519:YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8": "YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"u8VOi4IaeRJwDgy2ftK02NJQPdBijy8f/0+WnHGG72yfOvMthwWzEw8SrRSNG8glBNrfHinKwCyJJzAJwyepCQ",
},
},
},
},
};
/**
* A `/room_keys/version` response containing the current server-side backup info.
* To be used during tests with fetchmock.
*/
const BACKUP_RESPONSE: any = {
auth_data: {
public_key: "q+HZiJdHl2Yopv9GGvv7EYSzDMrAiRknK4glSdoaomI",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"reDp6Mu+j+tfUL3/T6f5OBT3N825Lzpc43vvG+RvjX6V+KxXzodBQArgCoeEHLtL9OgSBmNrhTkSOX87MWCKAw",
"ed25519:KMFSTJSMLB":
"F8tyV5W6wNi0GXTdSg+gxSCULQi0EYxdAAqfkyNq58KzssZMw5i+PRA0aI2b+D7NH/aZaJrtiYNHJ0gWLSQvAw",
},
},
},
version: "7",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "1",
count: 79,
};
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const FULL_ACCOUNT_DATASET: DumpDataSetInfo = {
userId: "@vdhtest200713:matrix.org",
deviceId: "KMFSTJSMLB",
pickleKey: "+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o",
backupResponse: BACKUP_RESPONSE,
keyQueryResponse: KEYS_QUERY_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/full_account/dump.json",
};
@@ -18,11 +18,12 @@ import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
/**
* Populate an IndexedDB store with the test data from this directory.
* Populate an IndexedDB store with a set of test data.
*
* @param name - Name of the IndexedDB database to create.
* @param dumpPath - The path to the dump file to import.
*/
export async function populateStore(name: string): Promise<IDBDatabase> {
export async function populateStore(name: string, dumpPath: string): Promise<IDBDatabase> {
const req = indexedDB.open(name, 11);
const db = await new Promise<IDBDatabase>((resolve, reject) => {
@@ -42,7 +43,7 @@ export async function populateStore(name: string): Promise<IDBDatabase> {
};
});
await importData(db);
await importData(db, dumpPath);
return db;
}
@@ -100,8 +101,8 @@ function upgradeDatabase(oldVersion: number, db: IDBDatabase) {
}
}
async function importData(db: IDBDatabase) {
const path = resolve("spec/test-utils/test_indexeddb_cryptostore_dump/dump.json");
async function importData(db: IDBDatabase, dumpPath: string) {
const path = resolve(dumpPath);
const json: Record<string, Array<{ key?: any; value: any }>> = JSON.parse(
await readFile(path, { encoding: "utf8" }),
);
@@ -134,3 +135,20 @@ async function importData(db: IDBDatabase) {
});
}
}
export interface DumpDataSetInfo {
/** The user ID to use for the test.*/
userId: string;
/** The device ID to use for the test.*/
deviceId: string;
/** The path to the dump file to import via {@link populateStore}.*/
dumpPath: string;
/** The pickle key to use for the dumped account.*/
pickleKey: string;
/** The response to use for the keys query. */
keyQueryResponse: any;
/** The response to use for the backup query.*/
backupResponse?: any;
/** Additional dump info specific for some tests.*/
[key: string]: any;
}
@@ -0,0 +1,4 @@
## Dump of a libolm indexeddb cryptostore where the msk is not cached
A dump simulating an account where the identity was verified, but the msk was not in cache.
Used to test that the owner identity local trust is migrated correctly.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,299 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
import { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEY_QUERY_RESPONSE: any = {
device_keys: {
"@migration:localhost": {
CBGTADUILV: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "CBGTADUILV",
keys: {
"curve25519:CBGTADUILV": "gqhFlc7Wzc1wmmmAu3ySIEe4LtDcBK/bdzrtZg+mMSg",
"ed25519:CBGTADUILV": "q1q3L1Il4l61c/6TmI4fYWMsseNMJJYE2Y0r+5ajKQI",
},
signatures: {
"@migration:localhost": {
"ed25519:CBGTADUILV":
"ppSmA0slyQ7RJOFn+qZSLCGeHN6/jAmqKvUZo5Q1hWk0ugkKycRoSUi9TOfbfAVSf8xvFirXy2VGXQbEVPJqAA",
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
"cFLWl1fjehLrzrEn3UnmZMIgy3C23WMgGRsn4e6Z/55vmen4KMs8bLpgZaDoWhIdn/8siHRWafA5sFdzK2NsBQ",
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
"C6EeqNPcaQyuZgo8+HOUywc/TMkW5IMjg7aoxyu93X//KcNNXKRfj1banYP6XqyPuQITLamBYc1089Jpt9g4Cw",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"YwBN/SbCxO8hPgv1B9JY2WVFK4LNK9vq1UNVrkF2j0ZDw9LrvaOws72mbmzZ0nbD3ohcEZ8rXsEosxEVr5r7AQ",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
TMWBMDZPFT: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "TMWBMDZPFT",
keys: {
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
},
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
},
},
failures: {},
master_keys: {
"@migration:localhost": {
keys: {
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4": "cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4",
},
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"RrPUnYoekK7wZGrLNXshgoupF8v53S/vJyvkBJi+q9THh4Qrf3CieuVJFx8mwtmEZgGoA2tSroAVnRqvEQ+IBQ",
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
"o4CbtdU3IqJK90UXAEBtxps2m4XBYvWJI2nbVlzBaGRr+Xt/3vtwDMlc5G970kPQWBbs/koYJh8MSaE7Fm1mAg",
"ed25519:CBGTADUILV":
"AgZoG+ix8aW3FAW6v+/Xu+QJpxzvsx5itbB8RyqMet9YlNqX90vYIbBV7IoV2WFY2WdANYEffX2CE0FpR6NnCg",
},
},
usage: ["master"],
user_id: "@migration:localhost",
},
},
self_signing_keys: {
"@migration:localhost": {
keys: {
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4": "RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4",
},
signatures: {
"@migration:localhost": {
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
"hs8VqoTfipDjC2pzFdmzb1aENhDjVV+gc86fuYftczaCcsXUWop/NPwoF51Ie6Nb3YL0N7ZZAUrycuJP5hFbDg",
},
},
usage: ["self_signing"],
user_id: "@migration:localhost",
},
},
user_signing_keys: {
"@migration:localhost": {
keys: {
"ed25519:WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8": "WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8",
},
signatures: {
"@migration:localhost": {
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
"Vlba5rJQxG+ussVLoycvHcin7Ghv0uUeClDqDbM+RPF+jx9w4ozbcuEOTJdyzyPA+GxN9Kzh2lmVFMMQGyvNAw",
},
},
usage: ["user_signing"],
user_id: "@migration:localhost",
},
},
};
/**
* A new key query response for the same user simulating a cross-signing key reset.
* To be used during tests with fetchmock.
*/
const ROTATED_KEY_QUERY_RESPONSE: any = {
device_keys: {
"@migration:localhost": {
TMWBMDZPFT: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "TMWBMDZPFT",
keys: {
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
},
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
XFZFSCUOFL: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "XFZFSCUOFL",
keys: {
"curve25519:XFZFSCUOFL": "aN2Ty+0rutNkrRtxhV+ciI8GhF4epSxzL7bAOr8zfkc",
"ed25519:XFZFSCUOFL": "V7CPhXdfLFk+qAOFivrpFskmunVTeuM+EOM3DMlDxkI",
},
signatures: {
"@migration:localhost": {
"ed25519:XFZFSCUOFL":
"4Pqc2FWJ5p/L/tSlfUBIlcQzLmN5CksJriAibY8LSDAXdGYiQJ7hvKqneEuVhrMYwqyIxb4bAad+r6wnY0/7Cg",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"yH8pKnD+E8YaawS+1NCjwy0cf2WzBRff9BBNX4YnAuTyc6s5b1QqNfu9DP5qblw8TZ7hZmaziePZKsjRiqJLBg",
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y":
"M8SfAiEUzd7AsWp8InS7BxV3cRqV3MjMxks4DwSxsVxvkCco2JWybKgev+vTZyM6XDg930o0FObQOxWm4+CkBw",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
},
},
failures: {},
master_keys: {
"@migration:localhost": {
user_id: "@migration:localhost",
usage: ["master"],
keys: {
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack": "rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack",
},
signatures: {
"@migration:localhost": {
"ed25519:XFZFSCUOFL":
"C8aswtyUABWvj2DInehVoh2P/EDbwRhlIk51LtV3L71POUCh7pZuyXRMMWKZeyRvHRmEllXBtRkH1iol/p56Bg",
},
},
},
},
self_signing_keys: {
"@migration:localhost": {
user_id: "@migration:localhost",
usage: ["self_signing"],
keys: {
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y": "OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y",
},
signatures: {
"@migration:localhost": {
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"dH596pGp8+f8dlwd81UrKDWoRDd24yAqqMSLqR4fJHyfszbn7qCvQA6LYZ023TLmk33FKcJqRtd2v/ykTmS3Bg",
},
},
},
},
user_signing_keys: {
"@migration:localhost": {
user_id: "@migration:localhost",
usage: ["user_signing"],
keys: {
"ed25519:8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o": "8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o",
},
signatures: {
"@migration:localhost": {
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"FX6ylagvx3IG1zMf/ayYgDb/1+x0/F28pHQqzQMGGssAmc15nat/R6AF0QO7Qg7uqTAf04ohuZtWax3dTwjNDQ",
},
},
},
},
};
/**
* A `/room_keys/version` response containing the current server-side backup info.
* To be used during tests with fetchmock.
*/
const BACKUP_RESPONSE: KeyBackupInfo = {
auth_data: {
public_key: "2ffIfIB4oryqZpsJQjQNUaxgCzxliC6A4PJvnrN+XAA",
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"qBvalid/G4hnSF3hAeX4TtRN6/BqprgiYnLEtDuatyQ5WxWr0s4uSOyvHSglsRdpoo32FDBHfTIZkCOVxSLwAA",
},
},
},
version: "2",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "0",
count: 0,
};
/**
* This was generated by doing a backup reset on the account.
* This is a new valid backup for this account.
*/
const NEW_BACKUP_RESPONSE: KeyBackupInfo = {
auth_data: {
public_key: "CkDxWALi3lcChgjEZFEM6clYq5x768XBwsL++eaOzTI",
signatures: {
"@migration:localhost": {
"ed25519:YVEGEYPYWX":
"ZSYuQDdwgB9WKXQ+z5aWWfqSolBCGRw53kur1Vy956gFefgzCBkMbw5M0I2UgfU2Cukri7jZ4ig201zmLNmaAA",
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"+UQ8EA507LoIqgK9rPsqPoGrj+iRBJeY2Oz0mMtXmVf8c1y8G0KWJNUWqvOysnOhsoJf1bt8ey48CxjjtSQ2AA",
},
},
},
version: "3",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "0",
count: 0,
};
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const MSK_NOT_CACHED_DATASET: DumpDataSetInfo = {
userId: "@migration:localhost",
deviceId: "CBGTADUILV",
pickleKey: "qEURMepfkMvoBQGaWlI9MZKYnDMsSAiW8aFTKXaeDV0",
keyQueryResponse: KEY_QUERY_RESPONSE,
rotatedKeyQueryResponse: ROTATED_KEY_QUERY_RESPONSE,
backupResponse: BACKUP_RESPONSE,
newBackupResponse: NEW_BACKUP_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump/dump.json",
};
@@ -0,0 +1,4 @@
## Dump of a libolm indexeddb cryptostore where the identity is not trusted.
A dump of an account where the identity was not verified.
Used as a test case for migration of the identity local trust.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,126 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEY_QUERY_RESPONSE = {
device_keys: {
"@untrusted:localhost": {
IXNYALOZWU: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "IXNYALOZWU",
keys: {
"curve25519:IXNYALOZWU": "EHMQEtJd9INJg28HwKK8Te1EX8obR3VTtyNwf/rcczM",
"ed25519:IXNYALOZWU": "OxMfZHsYJvroTp1RtjUOejpWbRBryN6VsojC5dKR74U",
},
signatures: {
"@untrusted:localhost": {
"ed25519:IXNYALOZWU":
"tWaTiRKc95ZCqM2qrKTdq1sQ3DPFgw3vdrOVmWIHQwj92DCgJtnQ9uymLMOq+MSb80bdBBjXwrNeOufgaL/6CQ",
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg":
"+QXZFLiAv+k7UXgAP6AXLk/PdZ3TlJ77M23m73v8qvavAlnkLBAjKNA3BG39JTQET5UhW5DnCohwsbGP+aY1Cw",
},
},
user_id: "@untrusted:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
VJPSPVPWZT: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "VJPSPVPWZT",
keys: {
"curve25519:VJPSPVPWZT": "+RxCNRFPqBZJm6PLjEJsSdFixGWQJygD5Os11/+6PC0",
"ed25519:VJPSPVPWZT": "wqH7xK/DQya8m05Vy4rnacjugGNBiY+7Ml6wyRVkM9U",
},
signatures: {
"@untrusted:localhost": {
"ed25519:VJPSPVPWZT":
"XC+RoKL/zVZOIwk/bGEQJlJu49QicY1v6vSDMHA2y0/fpX/MD4KiWGD7+W5DFD54E8FrFVTsIgkzat561qdTBQ",
},
},
user_id: "@untrusted:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
},
},
failures: {},
master_keys: {
"@untrusted:localhost": {
keys: {
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g": "Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g",
},
signatures: {
"@untrusted:localhost": {
"ed25519:IXNYALOZWU":
"KdAdyKO2sb3Di3bdK+oxf+gjMSmW/sisRNvpKZORPKwmy2SGaKGYkecBtslunoFjnb+hjIESgweQu6cHoNX4AA",
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
"b0R9Id5HxHYo+MA22Vlq0OckTrWnSWhgHLvF8Wr4e154JdtOyK7N0aXPQPkrLB0fmyVmGdbDa9xs9jsfINGmDw",
},
},
usage: ["master"],
user_id: "@untrusted:localhost",
},
},
self_signing_keys: {
"@untrusted:localhost": {
keys: {
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg": "+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg",
},
signatures: {
"@untrusted:localhost": {
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
"z/5z51jbRpyDQhYnfUHhhb5fUbzRDlfjD8mZA2ZGStpE/F41lDyxjlvF2W/E2CJ27bmJFdk7nC+ZCwriYfYxBw",
},
},
usage: ["self_signing"],
user_id: "@untrusted:localhost",
},
},
user_signing_keys: {
"@untrusted:localhost": {
keys: {
"ed25519:L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ": "L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ",
},
signatures: {
"@untrusted:localhost": {
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
"UuNvzebLQn31LYGbx+ADe60BB25kWy4SVVyd9BXlY/tAZMoA8Tmq1e2R2tJJtPdJxC/Oogktj2+iikZV/YMjAQ",
},
},
usage: ["user_signing"],
user_id: "@untrusted:localhost",
},
},
};
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const IDENTITY_NOT_TRUSTED_DATASET: DumpDataSetInfo = {
userId: "@untrusted:localhost",
deviceId: "VJPSPVPWZT",
pickleKey: "WVllQb4Lk/WwP4Q7iBfeTUHpgydZm9YqXI1B5bTvnIM",
keyQueryResponse: KEY_QUERY_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/unverified/dump.json",
};
+2 -99
View File
@@ -15,13 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import MockHttpBackend from "matrix-mock-request";
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
import { AutoDiscoveryAction } from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
import { OidcError } from "../../src/oidc/error";
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
// keep to reset the fetch function after using MockHttpBackend
// @ts-ignore private property
@@ -409,10 +406,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -450,10 +443,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -476,9 +465,6 @@ describe("AutoDiscovery", function () {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
invalid: true,
},
});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -494,10 +480,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "FAIL_ERROR",
error: OidcError.Misconfigured,
},
};
expect(conf).toEqual(expected);
@@ -728,10 +710,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: "https://identity.example.org",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -784,10 +762,6 @@ describe("AutoDiscovery", function () {
"org.example.custom.property": {
cupcakes: "yes",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -883,7 +857,7 @@ describe("AutoDiscovery", function () {
const expected = {
"m.homeserver": {
state: AutoDiscoveryAction.FAIL_ERROR,
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
error: AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION,
base_url: "https://example.org",
},
"m.identity_server": {
@@ -897,75 +871,4 @@ describe("AutoDiscovery", function () {
}),
]);
});
describe("m.authentication", () => {
const homeserverName = "example.org";
const homeserverUrl = "https://chat.example.org/";
const issuer = "https://auth.org/";
beforeAll(() => {
// make these tests independent from fetch mocking above
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
beforeEach(() => {
fetchMock.resetBehavior();
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.5"] });
fetchMock.get("https://example.org/.well-known/matrix/client", {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
issuer,
},
});
});
it("should return valid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.SUCCESS,
...config,
signingKeys: [],
account: undefined,
error: null,
});
});
it("should set state to error for invalid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
// authorization_code is required
config.metadata.grant_types_supported = ["openid"];
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.FAIL_ERROR,
error: OidcError.OpSupport,
});
});
});
});
+5 -4
View File
@@ -50,17 +50,18 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
});
it("Should encode unpadded URL-safe base64", () => {
const toEncode = "?????";
// Chosen to have padding and multiple instances of / and + in the base64
const toEncode = "???????⊕⊗⊗";
const data = new TextEncoder().encode(toEncode);
const encoded = encodeUnpaddedBase64Url(data);
expect(encoded).toEqual("Pz8_Pz8");
expect(encoded).toEqual("Pz8_Pz8_P-KKleKKl-KKlw");
});
it("Should decode URL-safe base64", () => {
const decoded = new TextDecoder().decode(decodeBase64("Pz8_Pz8="));
const decoded = new TextDecoder().decode(decodeBase64("Pz8_Pz8_P-KKleKKl-KKlw=="));
expect(decoded).toStrictEqual("?????");
expect(decoded).toStrictEqual("???????⊕⊗⊗");
});
it("Encode unpadded should not have padding", () => {
+22
View File
@@ -76,5 +76,27 @@ describe("ContentRepo", function () {
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
);
});
it("should return an authenticated URL when requested", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
baseUrl + "/_matrix/client/v1/media/download/server.name/resourceid?allow_redirect=true",
);
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, true, true)).toEqual(
baseUrl +
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
it("should force-enable allow_redirects when useAuthentication is set true", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, false, true)).toEqual(
baseUrl + "/_matrix/client/v1/media/download/server.name/resourceid?allow_redirect=true",
);
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, false, true)).toEqual(
baseUrl +
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
});
});
+3 -2
View File
@@ -25,6 +25,7 @@ import { UserTrustLevel } from "../../src/crypto/CrossSigning";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
import * as testData from "../test-utils/test-data";
import { KnownMembership } from "../../src/@types/membership";
const Olm = global.Olm;
@@ -463,7 +464,7 @@ describe("Crypto", function () {
type: "m.room.member",
sender: "@alice:example.com",
room_id: roomId,
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
state_key: "@bob:example.com",
}),
]);
@@ -795,7 +796,7 @@ describe("Crypto", function () {
type: "m.room.member",
sender: "@clara:example.com",
room_id: roomId,
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
state_key: "@bob:example.com",
}),
]);
+3 -2
View File
@@ -36,6 +36,7 @@ import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
import { recursiveMapToObject } from "../../../../src/utils";
import { sleep } from "../../../../src/utils";
import { KnownMembership } from "../../../../src/@types/membership";
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
@@ -806,11 +807,11 @@ describe("MegolmDecryption", function () {
aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([
{
userId: "@alice:example.com",
membership: "join",
membership: KnownMembership.Join,
},
{
userId: "@bob:example.com",
membership: "join",
membership: KnownMembership.Join,
},
]);
const BOB_DEVICES = {
+10 -10
View File
@@ -23,12 +23,12 @@ import HttpBackend from "matrix-mock-request";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixError } from "../../../src/http-api";
import { logger } from "../../../src/logger";
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { CryptoEvent } from "../../../src/crypto";
import { IDevice } from "../../../src/crypto/deviceinfo";
import { TestClient } from "../../TestClient";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BootstrapCrossSigningOpts } from "../../../src/crypto-api";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../../../src/crypto-api";
const PUSH_RULES_RESPONSE: Response = {
method: "GET",
@@ -377,7 +377,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -515,7 +515,7 @@ describe("Cross Signing", function () {
};
olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", "");
const bobMaster: ICrossSigningKey = {
const bobMaster: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
@@ -630,7 +630,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -696,7 +696,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -763,7 +763,7 @@ describe("Cross Signing", function () {
const bobSigning2 = new global.Olm.PkSigning();
const bobPrivkey2 = bobSigning2.generate_seed();
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
const bobSSK2: ICrossSigningKey = {
const bobSSK2: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -913,7 +913,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -988,7 +988,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -1048,7 +1048,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
+6 -5
View File
@@ -23,12 +23,13 @@ import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { ISignatures } from "../../../src/@types/signed";
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";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@@ -475,7 +476,7 @@ describe("Secrets", function () {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign<ICrossSigningKey>(
self_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
@@ -486,7 +487,7 @@ describe("Secrets", function () {
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
user_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
@@ -631,7 +632,7 @@ describe("Secrets", function () {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign<ICrossSigningKey>(
self_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
@@ -642,7 +643,7 @@ describe("Secrets", function () {
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
user_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
@@ -0,0 +1,73 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "fake-indexeddb/auto";
import { IndexedDBCryptoStore } from "../../../../src";
import { MigrationState } from "../../../../src/crypto/store/base";
describe("IndexedDBCryptoStore", () => {
describe("Test `existsAndIsNotMigrated`", () => {
beforeEach(async () => {
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
it("Should be true if there is a legacy database", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
await store.startup();
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(true);
});
it("Should be true if there is a legacy database in non migrated state", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
await store.startup();
await store.setMigrationState(MigrationState.NOT_STARTED);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(true);
});
describe.each([
MigrationState.INITIAL_DATA_MIGRATED,
MigrationState.OLM_SESSIONS_MIGRATED,
MigrationState.MEGOLM_SESSIONS_MIGRATED,
MigrationState.ROOM_SETTINGS_MIGRATED,
])("Exists and Migration state is %s", (migrationState) => {
it("Should be false if migration has started", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
await store.startup();
await store.setMigrationState(migrationState);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(false);
});
});
it("Should be false if there is no legacy database", async () => {
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
expect(result).toBe(false);
});
});
});
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { encodeUnpaddedBase64Url } from "../../src";
import { sha256 } from "../../src/digest";
describe("sha256", () => {
it("should hash a string", async () => {
const hash = await sha256("test");
expect(encodeUnpaddedBase64Url(hash)).toBe("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg");
});
it("should hash a string with emoji", async () => {
const hash = await sha256("test 🍱");
expect(encodeUnpaddedBase64Url(hash)).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw");
});
it("throws if webcrypto is not available", async () => {
const oldCrypto = global.crypto;
try {
global.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
global.crypto = oldCrypto;
}
});
});
+267 -6
View File
@@ -32,7 +32,7 @@ import {
IOpenIDCredentials,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
@@ -59,8 +59,26 @@ class MockWidgetApi extends EventEmitter {
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendRoomEvent = jest.fn(
(eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public sendStateEvent = jest.fn(
(
eventType: string,
stateKey: string,
content: unknown,
roomId?: string,
delay?: number,
parentDelayId?: string,
) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public updateDelayedEvent = jest.fn();
public sendToDevice = jest.fn();
public requestOpenIDConnectToken = jest.fn(() => {
return testOIDCToken;
@@ -75,6 +93,20 @@ class MockWidgetApi extends EventEmitter {
public transport = { reply: jest.fn() };
}
declare module "../../src/types" {
interface StateEvents {
"org.example.foo": {
hello: string;
};
}
interface TimelineEvents {
"org.matrix.rageshake_request": {
request_id: number;
};
}
}
describe("RoomWidgetClient", () => {
let widgetApi: MockedObject<WidgetApi>;
let client: MatrixClient;
@@ -87,9 +119,12 @@ describe("RoomWidgetClient", () => {
client.stopClient();
});
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
const makeClient = async (
capabilities: ICapabilities,
sendContentLoaded: boolean | undefined = undefined,
): Promise<void> => {
const baseUrl = "https://example.org";
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }, sendContentLoaded);
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
widgetApi.emit("ready");
await client.startClient();
@@ -108,6 +143,17 @@ describe("RoomWidgetClient", () => {
);
});
it("send handles wrong field in response", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow();
});
it("receives", async () => {
const event = new MatrixEvent({
type: "org.matrix.rageshake_request",
@@ -143,7 +189,200 @@ describe("RoomWidgetClient", () => {
});
});
describe("messages", () => {
describe("delayed events", () => {
describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("sends delayed message events", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
await client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
);
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
2000,
undefined,
);
});
it("sends child action delayed message events", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
const parentDelayId = `id-${Math.random()}`;
await client._unstable_sendDelayedEvent(
"!1:example.org",
{ parent_delay_id: parentDelayId },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
);
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
undefined,
parentDelayId,
);
});
it("sends delayed state events", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
await client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
);
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
2000,
undefined,
);
});
it("sends child action delayed state events", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
const parentDelayId = `fg-${Math.random()}`;
await client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ parent_delay_id: parentDelayId },
"org.example.foo",
{ hello: "world" },
"bar",
);
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
undefined,
parentDelayId,
);
});
it("send delayed message events handles wrong field in response", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
),
).rejects.toThrow();
});
it("send delayed state events handles wrong field in response", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
),
).rejects.toThrow();
});
it("updates delayed events", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await client._unstable_updateDelayedEvent("id", action);
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
}
});
});
describe("when unsupported", () => {
it("fails to send delayed message events", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
await expect(
client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
),
).rejects.toThrow("Server does not support");
});
it("fails to send delayed state events", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
await expect(
client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
),
).rejects.toThrow("Server does not support");
});
it("fails to update delayed state events", async () => {
await makeClient({});
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await expect(client._unstable_updateDelayedEvent("id", action)).rejects.toThrow(
"Server does not support",
);
}
});
});
});
describe("initialization", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
@@ -158,6 +397,15 @@ describe("RoomWidgetClient", () => {
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
});
it("sends content loaded when configured", async () => {
await makeClient({});
expect(widgetApi.sendContentLoaded).toHaveBeenCalled();
});
it("does not sent content loaded when configured", async () => {
await makeClient({}, false);
expect(widgetApi.sendContentLoaded).not.toHaveBeenCalled();
});
// No point in testing sending and receiving since it's done exactly the
// same way as non-message events
});
@@ -185,6 +433,17 @@ describe("RoomWidgetClient", () => {
);
});
it("send handles incorrect response", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"),
).rejects.toThrow();
});
it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
@@ -305,12 +564,14 @@ describe("RoomWidgetClient", () => {
expect(await emittedSync).toEqual(SyncState.Syncing);
});
});
describe("oidc token", () => {
it("requests an oidc token", async () => {
await makeClient({});
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
});
});
it("gets TURN servers", async () => {
const server1: ITurnServer = {
uris: [
-5
View File
@@ -136,11 +136,6 @@ describe("EventTimelineSet", () => {
expect(eventsInLiveTimeline.length).toStrictEqual(1);
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
});
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Replace, false)).not.toThrow();
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Ignore, true)).not.toThrow();
});
});
describe("addEventToTimeline", () => {
+11 -10
View File
@@ -7,6 +7,7 @@ import { MatrixClient } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { RoomMember } from "../../src/models/room-member";
import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { KnownMembership } from "../../src/@types/membership";
describe("EventTimeline", function () {
const roomId = "!foo:bar";
@@ -50,7 +51,7 @@ describe("EventTimeline", function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
@@ -87,7 +88,7 @@ describe("EventTimeline", function () {
const state = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
@@ -203,11 +204,11 @@ describe("EventTimeline", function () {
it("should set event.sender for new and old events", function () {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
@@ -246,11 +247,11 @@ describe("EventTimeline", function () {
it("should set event.target for new and old m.room.member events", function () {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
@@ -267,14 +268,14 @@ describe("EventTimeline", function () {
const newEv = utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
});
const oldEv = utils.mkMembership({
room: roomId,
mship: "ban",
mship: KnownMembership.Ban,
user: userB,
skey: userA,
event: true,
@@ -291,7 +292,7 @@ describe("EventTimeline", function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
@@ -330,7 +331,7 @@ describe("EventTimeline", function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
+414 -25
View File
@@ -22,6 +22,7 @@ import { Filter } from "../../src/filter";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
import {
EventType,
MsgType,
RelationType,
RoomCreateTypeField,
RoomType,
@@ -38,30 +39,32 @@ import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import {
ContentHelpers,
ClientPrefix,
ConditionKind,
ContentHelpers,
Direction,
EventTimeline,
EventTimelineSet,
getHttpUriForMxc,
ICreateRoomOpts,
IPushRule,
IRequestOpts,
MatrixError,
MatrixHttpApi,
MatrixScheduler,
Method,
Room,
EventTimelineSet,
PushRuleActionName,
TweakName,
Room,
RuleId,
IPushRule,
ConditionKind,
getHttpUriForMxc,
TweakName,
UpdateDelayedEventAction,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyRecommendation,
PolicyScope,
} from "../../src/models/invites-ignorer";
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
@@ -71,6 +74,8 @@ import * as featureUtils from "../../src/feature";
import { StubStore } from "../../src/store/stub";
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { KnownMembership } from "../../src/@types/membership";
import { RoomMessageEventContent } from "../../src/@types/events";
jest.useFakeTimers();
@@ -93,7 +98,7 @@ type HttpLookup = {
method: string;
path: string;
prefix?: string;
data?: Record<string, any>;
data?: Record<string, any> | Record<string, any>[];
error?: object;
expectBody?: Record<string, any>;
expectQueryParams?: QueryDict;
@@ -294,7 +299,9 @@ describe("MatrixClient", function () {
...(opts || {}),
});
// FIXME: We shouldn't be yanking http like this.
client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => {
client.http = (
["authedRequest", "getContentUri", "request", "uploadContent", "idServerRequest"] as const
).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixHttpApi<any>);
@@ -382,6 +389,9 @@ describe("MatrixClient", function () {
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true)).toBe(
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true),
);
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true, true)).toBe(
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true, true),
);
});
});
@@ -565,7 +575,7 @@ describe("MatrixClient", function () {
describe("sendEvent", () => {
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body };
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
@@ -660,12 +670,13 @@ describe("MatrixClient", function () {
const content = {
body,
"msgtype": MsgType.Text,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
};
} satisfies RoomMessageEventContent;
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
@@ -696,6 +707,328 @@ describe("MatrixClient", function () {
});
});
describe("_unstable_sendDelayedEvent", () => {
const unstableMSC4140Prefix = `${ClientPrefix.Unstable}/org.matrix.msc4140`;
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
const timeoutDelayOpts = { delay: 2000 };
const realTimeoutDelayOpts = { "org.matrix.msc4140.delay": 2000 };
beforeEach(() => {
unstableFeatures["org.matrix.msc4140"] = true;
});
it("throws when unsupported by server", async () => {
unstableFeatures["org.matrix.msc4140"] = false;
const errorMessage = "Server does not support";
await expect(
client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
null,
EventType.RoomMessage,
{ ...content },
client.makeTxnId(),
),
).rejects.toThrow(errorMessage);
await expect(
client._unstable_sendDelayedStateEvent(roomId, timeoutDelayOpts, EventType.RoomTopic, {
topic: "topic",
}),
).rejects.toThrow(errorMessage);
await expect(client._unstable_getDelayedEvents()).rejects.toThrow(errorMessage);
await expect(
client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send),
).rejects.toThrow(errorMessage);
});
it("works with null threadId", async () => {
httpLookups = [];
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody: content,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
null,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody: content,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
null,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("works with non-null threadId", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const expectBody = {
...content,
"m.relates_to": {
event_id: threadId,
is_falling_back: true,
rel_type: "m.thread",
},
};
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("should add thread relation if threadId is passed and the relation is missing", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const expectBody = {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: threadId,
},
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
};
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const content = {
body,
"msgtype": MsgType.Text,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
} satisfies RoomMessageEventContent;
const expectBody = {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
},
};
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("can send a delayed state event", async () => {
httpLookups = [];
const content = { topic: "The year 2000" };
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody: content,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedStateEvent(
roomId,
timeoutDelayOpts,
EventType.RoomTopic,
{ ...content },
);
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody: content,
});
await client._unstable_sendDelayedStateEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
EventType.RoomTopic,
{ ...content },
);
});
it("can look up delayed events", async () => {
httpLookups = [
{
method: "GET",
prefix: unstableMSC4140Prefix,
path: "/delayed_events",
data: [],
},
];
await client._unstable_getDelayedEvents();
});
it("can update delayed events", async () => {
const delayId = "id";
const action = UpdateDelayedEventAction.Restart;
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`,
data: {
action,
},
},
];
await client._unstable_updateDelayedEvent(delayId, action);
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
@@ -750,7 +1083,7 @@ describe("MatrixClient", function () {
it("should get (unstable) file trees with valid state", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
@@ -789,7 +1122,7 @@ describe("MatrixClient", function () {
it("should not get (unstable) file trees if not joined", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "leave", // "not join"
getMyMembership: () => KnownMembership.Leave, // "not join"
} as unknown as Room;
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
@@ -812,7 +1145,7 @@ describe("MatrixClient", function () {
it("should not get (unstable) file trees with invalid create contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
@@ -849,7 +1182,7 @@ describe("MatrixClient", function () {
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
@@ -955,7 +1288,7 @@ describe("MatrixClient", function () {
const filter = new Filter(client.credentials.userId);
const filterId = await client.getOrCreateFilter(filterName, filter);
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
expect(filterId).toEqual(!Array.isArray(FILTER_RESPONSE.data) && FILTER_RESPONSE.data?.filter_id);
});
});
@@ -1309,7 +1642,7 @@ describe("MatrixClient", function () {
describe("redactEvent", () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomEncryption) {
@@ -1448,7 +1781,7 @@ describe("MatrixClient", function () {
const txnId = "m12345";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status),
hasEncryptionStateEvent: jest.fn().mockReturnValue(true),
} as unknown as Room;
@@ -1526,8 +1859,6 @@ describe("MatrixClient", function () {
{ startOpts: {}, hasThreadSupport: false },
{ startOpts: { threadSupport: true }, hasThreadSupport: true },
{ startOpts: { threadSupport: false }, hasThreadSupport: false },
{ startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true },
{ startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false },
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
await client.startClient(startOpts);
expect(client.supportsThreads()).toBe(hasThreadSupport);
@@ -1554,7 +1885,6 @@ describe("MatrixClient", function () {
},
},
event_id: "$ev1",
user_id: "@alice:matrix.org",
});
expect(rootEvent.isThreadRoot).toBe(true);
@@ -2081,10 +2411,10 @@ describe("MatrixClient", function () {
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
// Add a rule in the new source room.
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, EventType.PolicyRuleUser, {
entity: "*:example.org",
reason: "just a test",
recommendation: "m.ban",
recommendation: PolicyRecommendation.Ban,
});
// We should reject this invite.
@@ -2171,8 +2501,8 @@ describe("MatrixClient", function () {
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId) as WrappedRoom;
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
expect(targetRoom._state.get(EventType.PolicyRuleUser)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(EventType.PolicyRuleUser)?.[eventId]).toBeFalsy();
});
});
@@ -3012,4 +3342,63 @@ describe("MatrixClient", function () {
expect(result).toEqual({});
});
});
describe("getAuthIssuer", () => {
it("should use unstable prefix", async () => {
httpLookups = [
{
method: "GET",
path: `/auth_issuer`,
data: {
issuer: "https://issuer/",
},
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
await expect(client.getAuthIssuer()).resolves.toEqual({ issuer: "https://issuer/" });
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";
client.http.idServerRequest = jest.fn().mockImplementation((method, path, params) => {
if (method === "GET" && path === "/hash_details") {
return { algorithms: ["sha256"], lookup_pepper: "carrot" };
} else if (method === "POST" && path === "/lookup") {
return {
mappings: {
"WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU": "@bob:homeserver.dummy",
},
};
}
throw new Error("Test impl doesn't know about this request");
});
const lookupResult = await client.identityHashedLookup([["bob@email.dummy", "email"]], ID_ACCESS_TOKEN);
expect(client.http.idServerRequest).toHaveBeenCalledWith(
"GET",
"/hash_details",
undefined,
"/_matrix/identity/v2",
ID_ACCESS_TOKEN,
);
expect(client.http.idServerRequest).toHaveBeenCalledWith(
"POST",
"/lookup",
{ pepper: "carrot", algorithm: "sha256", addresses: ["WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU"] },
"/_matrix/identity/v2",
ID_ACCESS_TOKEN,
);
expect(lookupResult).toHaveLength(1);
expect(lookupResult[0]).toEqual({ address: "bob@email.dummy", mxid: "@bob:homeserver.dummy" });
});
});
});
+155 -85
View File
@@ -15,16 +15,7 @@ limitations under the License.
*/
import { MatrixEvent } from "../../../src";
import { CallMembership, CallMembershipData } from "../../../src/matrixrtc/CallMembership";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
membershipID: "bloop",
};
import { CallMembership, CallMembershipDataLegacy, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
function makeMockEvent(originTs = 0): MatrixEvent {
return {
@@ -34,96 +25,175 @@ function makeMockEvent(originTs = 0): MatrixEvent {
}
describe("CallMembership", () => {
it("rejects membership with no expiry and no expires_ts", () => {
expect(() => {
new CallMembership(
makeMockEvent(),
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
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 }),
);
}).toThrow();
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("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);
});
it("considers memberships expired when local age large", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.localTimestamp = Date.now() - 6000;
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(true);
});
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]);
});
});
it("rejects membership with no device_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
}).toThrow();
});
describe("SessionMembershipData", () => {
const membershipTemplate: SessionMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
focus_active: { type: "livekit" },
foci_preferred: [{ type: "livekit" }],
};
it("rejects membership with no call_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: 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 scope", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).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("rejects membership with no call_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
}).toThrow();
});
it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
expect(membership.createdTs()).toEqual(12345);
});
it("allow membership with no scope", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).not.toThrow();
});
it("uses created_ts if present", () => {
const membership = new CallMembership(
makeMockEvent(12345),
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
);
expect(membership.createdTs()).toEqual(67890);
});
it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
expect(membership.createdTs()).toEqual(12345);
});
it("computes absolute expiry time based on expires", () => {
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
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_ts", () => {
const membership = new CallMembership(
makeMockEvent(1000),
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: 6000 }),
);
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
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);
});
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);
});
it("considers memberships expired when local age large", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.localTimestamp = Date.now() - 6000;
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(true);
});
it("returns active 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.getActiveFoci()).toEqual([mockFocus]);
it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(
fakeEvent,
Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }),
);
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
});
});
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
+60
View File
@@ -0,0 +1,60 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus";
describe("LivekitFocus", () => {
it("isLivekitFocus", () => {
expect(
isLivekitFocus({
type: "livekit",
livekit_service_url: "http://test.com",
livekit_alias: "test",
}),
).toBeTruthy();
expect(isLivekitFocus({ type: "livekit" })).toBeFalsy();
expect(
isLivekitFocus({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
).toBeFalsy();
expect(
isLivekitFocus({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }),
).toBeFalsy();
expect(
isLivekitFocus({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
).toBeFalsy();
});
it("isLivekitFocusActive", () => {
expect(
isLivekitFocusActive({
type: "livekit",
focus_selection: "oldest_membership",
}),
).toBeTruthy();
expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy();
expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
});
it("isLivekitFocusConfig", () => {
expect(
isLivekitFocusConfig({
type: "livekit",
livekit_service_url: "http://test.com",
}),
).toBeTruthy();
expect(isLivekitFocusConfig({ type: "livekit" })).toBeFalsy();
expect(isLivekitFocusConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
expect(isLivekitFocusConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
});
});
+305 -44
View File
@@ -15,7 +15,12 @@ limitations under the License.
*/
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { KnownMembership } from "../../../src/@types/membership";
import {
CallMembershipData,
CallMembershipDataLegacy,
SessionMembershipData,
} from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { randomString } from "../../../src/randomstring";
@@ -28,6 +33,7 @@ const membershipTemplate: CallMembershipData = {
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }],
};
const mockFocus = { type: "mock" };
@@ -40,6 +46,9 @@ describe("MatrixRTCSession", () => {
client = new MatrixClient({ baseUrl: "base_url" });
client.getUserId = jest.fn().mockReturnValue("@alice:example.org");
client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
client.doesServerSupportUnstableFeature = jest.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
});
afterEach(() => {
@@ -74,6 +83,13 @@ describe("MatrixRTCSession", () => {
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
});
it("ignores memberships events of members not in the room", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(0);
});
it("honours created_ts", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
expiredMembership.created_ts = 500;
@@ -90,19 +106,33 @@ describe("MatrixRTCSession", () => {
});
it("safely ignores events with no memberships section", () => {
const roomId = randomString(8);
const event = {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
};
const mockRoom = {
roomId: randomString(8),
...makeMockRoom([]),
roomId,
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
getStateEvents: (_type: string, _stateKey: string) => [
{
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
},
],
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_type: string, _stateKey: string) => [event],
events: new Map([
[
EventType.GroupCallMemberPrefix,
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
}),
}),
};
@@ -111,19 +141,33 @@ describe("MatrixRTCSession", () => {
});
it("safely ignores events with junk memberships section", () => {
const roomId = randomString(8);
const event = {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
};
const mockRoom = {
roomId: randomString(8),
...makeMockRoom([]),
roomId,
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
getStateEvents: (_type: string, _stateKey: string) => [
{
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({ memberships: "i am a fish" }),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
},
],
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_type: string, _stateKey: string) => [event],
events: new Map([
[
EventType.GroupCallMemberPrefix,
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
}),
}),
};
@@ -171,6 +215,93 @@ describe("MatrixRTCSession", () => {
expect(sess.memberships).toHaveLength(0);
});
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 sessionMembershipData: SessionMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA_session",
focus_active: mockFocus,
foci_preferred: [mockFocus],
};
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
beforeEach(() => {
sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
sentDelayedState = new Promise((resolve) => {
sendDelayedStateMock = jest.fn(() => {
resolve();
return {
delay_id: "id",
};
});
});
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
});
async function testSession(
membershipData: CallMembershipData[] | SessionMembershipData,
shouldUseLegacy: boolean,
): 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);
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 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);
});
});
describe("getOldestMembership", () => {
it("returns the oldest membership event", () => {
const mockRoom = makeMockRoom([
@@ -184,15 +315,88 @@ describe("MatrixRTCSession", () => {
});
});
describe("getsActiveFocus", () => {
const activeFociConfig = { type: "livekit", livekit_service_url: "https://active.url" };
it("gets the correct active focus with oldest_membership", () => {
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" }], {
type: "livekit",
focus_selection: "oldest_membership",
});
expect(sess.getActiveFocus()).toBe(activeFociConfig);
});
it("does not provide focus if the selction method is unknown", () => {
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" }], {
type: "livekit",
focus_selection: "unknown",
});
expect(sess.getActiveFocus()).toBe(undefined);
});
it("gets the correct active focus legacy", () => {
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);
});
});
describe("joining", () => {
let mockRoom: Room;
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sendEventMock: jest.Mock;
let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
beforeEach(() => {
sendStateEventMock = jest.fn();
sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
sentDelayedState = new Promise((resolve) => {
sendDelayedStateMock = jest.fn(() => {
resolve();
return {
delay_id: "id",
};
});
});
sendEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
client.sendEvent = sendEventMock;
mockRoom = makeMockRoom([]);
@@ -209,13 +413,15 @@ describe("MatrixRTCSession", () => {
});
it("shows joined once join is called", () => {
sess!.joinRoomSession([mockFocus]);
sess!.joinRoomSession([mockFocus], mockFocus);
expect(sess!.isJoined()).toEqual(true);
});
it("sends a membership event when joining a call", () => {
it("sends a membership event when joining a call", async () => {
const realSetTimeout = setTimeout;
jest.useFakeTimers();
sess!.joinRoomSession([mockFocus]);
sess!.joinRoomSession([mockFocus], mockFocus);
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
@@ -228,22 +434,65 @@ describe("MatrixRTCSession", () => {
device_id: "AAAAAAA",
expires: 3600000,
expires_ts: Date.now() + 3600000,
foci_active: [{ type: "mock" }],
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
},
"@alice:example.org",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
jest.useRealTimers();
});
describe("non-legacy calls", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
async function testJoin(useOwnedStateEvents: boolean): Promise<void> {
const realSetTimeout = setTimeout;
if (useOwnedStateEvents) {
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3779.default");
}
jest.useFakeTimers();
sess!.joinRoomSession([activeFocusConfig], activeFocus, { useLegacyMemberEvents: false });
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
foci_preferred: [activeFocusConfig],
focus_active: activeFocus,
} satisfies SessionMembershipData,
`${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`,
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
jest.useRealTimers();
}
it("sends a membership event with session payload when joining a non-legacy call", async () => {
await testJoin(false);
});
it("does not prefix the state key with _ for rooms that support user-owned state events", async () => {
await testJoin(true);
});
});
it("does nothing if join called when already joined", () => {
sess!.joinRoomSession([mockFocus]);
sess!.joinRoomSession([mockFocus], mockFocus);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
sess!.joinRoomSession([mockFocus]);
sess!.joinRoomSession([mockFocus], mockFocus);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
});
@@ -260,15 +509,26 @@ describe("MatrixRTCSession", () => {
const sendStateEventMock = jest.fn().mockImplementation(resolveFn);
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus]);
sess!.joinRoomSession([mockFocus], mockFocus);
const eventContent = await eventSentPromise;
// definitely should have renewed by 1 second before the expiry!
const timeElapsed = 60 * 60 * 1000 - 1000;
mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest
.fn()
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed));
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed);
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>) => {
@@ -294,7 +554,7 @@ describe("MatrixRTCSession", () => {
device_id: "AAAAAAA",
expires: 3600000 * 2,
expires_ts: 1000 + 3600000 * 2,
foci_active: [{ type: "mock" }],
foci_active: [mockFocus],
created_ts: 1000,
membershipID: expect.stringMatching(".*"),
},
@@ -308,7 +568,7 @@ describe("MatrixRTCSession", () => {
});
it("creates a key when joining", () => {
sess!.joinRoomSession([mockFocus], true);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA");
expect(keys).toHaveLength(1);
@@ -322,7 +582,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockImplementation(resolve);
});
sess!.joinRoomSession([mockFocus], true);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await eventSentPromise;
@@ -358,7 +618,7 @@ describe("MatrixRTCSession", () => {
});
});
sess!.joinRoomSession([mockFocus], true);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
jest.advanceTimersByTime(10000);
await eventSentPromise;
@@ -380,7 +640,7 @@ describe("MatrixRTCSession", () => {
throw e;
});
sess!.joinRoomSession([mockFocus], true);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
});
@@ -395,7 +655,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockImplementation(resolve);
});
sess.joinRoomSession([mockFocus], true);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
sendEventMock.mockClear();
@@ -448,7 +708,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
sess.joinRoomSession([mockFocus], true);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
const firstKeysPayload = await keysSentPromise1;
expect(firstKeysPayload.keys).toHaveLength(1);
@@ -475,7 +735,7 @@ describe("MatrixRTCSession", () => {
});
it("Doesn't re-send key immediately", async () => {
const realSetImmediate = setImmediate;
const realSetTimeout = setTimeout;
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
@@ -485,7 +745,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockImplementation(resolve);
});
sess.joinRoomSession([mockFocus], true);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
sendEventMock.mockClear();
@@ -503,7 +763,7 @@ describe("MatrixRTCSession", () => {
sess.onMembershipUpdate();
await new Promise((resolve) => {
realSetImmediate(resolve);
realSetTimeout(resolve);
});
expect(sendEventMock).not.toHaveBeenCalled();
@@ -581,7 +841,7 @@ describe("MatrixRTCSession", () => {
jest.advanceTimersByTime(10000);
sess.joinRoomSession([mockFocus]);
sess.joinRoomSession([mockFocus], mockFocus);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoomNoExpired!.roomId,
@@ -617,7 +877,7 @@ describe("MatrixRTCSession", () => {
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([mockFocus]);
sess.joinRoomSession([mockFocus], mockFocus);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
@@ -631,6 +891,7 @@ describe("MatrixRTCSession", () => {
device_id: "OTHERDEVICE",
expires: 3600000,
created_ts: 1000,
foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }],
membershipID: expect.stringMatching(".*"),
},
{
@@ -35,6 +35,7 @@ const membershipTemplate: CallMembershipData = {
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [{ type: "test" }],
};
describe("MatrixRTCSessionManager", () => {
@@ -87,7 +88,7 @@ describe("MatrixRTCSessionManager", () => {
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
it("Calls onCallEncryption on encryption keys event", () => {
it("Calls onCallEncryption on encryption keys event", async () => {
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
@@ -95,7 +96,7 @@ describe("MatrixRTCSessionManager", () => {
client.emit(ClientEvent.Room, room1);
const onCallEncryptionMock = jest.fn();
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
client.decryptEventIfNeeded = () => Promise.resolve();
const timelineEvent = {
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
getContent: jest.fn().mockReturnValue({}),
@@ -106,6 +107,7 @@ describe("MatrixRTCSessionManager", () => {
},
} as unknown as MatrixEvent;
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
await new Promise(process.nextTick);
expect(onCallEncryptionMock).toHaveBeenCalled();
});
});
+30 -9
View File
@@ -15,37 +15,58 @@ limitations under the License.
*/
import { EventType, MatrixEvent, Room } from "../../../src";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { randomString } from "../../../src/randomstring";
export function makeMockRoom(memberships: CallMembershipData[], localAge: number | null = null): Room {
type MembershipData = CallMembershipData[] | SessionMembershipData;
export function makeMockRoom(membershipData: MembershipData, localAge: number | null = null): Room {
const roomId = randomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(memberships, roomId, localAge);
const roomState = makeMockRoomState(membershipData, roomId, localAge);
return {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue(roomState),
}),
getVersion: jest.fn().mockReturnValue("default"),
} as unknown as Room;
}
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(memberships, roomId, localAge);
export function makeMockRoomState(membershipData: MembershipData, roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(membershipData, roomId, localAge);
return {
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_: string, stateKey: string) => {
if (stateKey !== undefined) return event;
return [event];
},
events: new Map([
[
event.getType(),
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
};
}
export function mockRTCEvent(memberships: CallMembershipData[], roomId: string, localAge: number | null): MatrixEvent {
export function mockRTCEvent(membershipData: MembershipData, roomId: string, localAge: number | null): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({
memberships: memberships,
}),
getContent: jest.fn().mockReturnValue(
!Array.isArray(membershipData)
? membershipData
: {
memberships: membershipData,
},
),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
localTimestamp: Date.now() - (localAge ?? 10),
+17 -18
View File
@@ -16,11 +16,12 @@ limitations under the License.
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
import { Room } from "../../../src/models/room";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
import { EventTimeline } from "../../../src/models/event-timeline";
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
import { EncryptedFile } from "../../../src/@types/media";
describe("MSC3089Branch", () => {
let client: MatrixClient;
@@ -254,7 +255,7 @@ describe("MSC3089Branch", () => {
it("should create new versions of itself", async () => {
const canaryName = "canary";
const canaryContents = "contents go here";
const canaryFile = {} as IEncryptedFile;
const canaryFile = {} as EncryptedFile;
const canaryAddl = { canary: true };
indexEvent.getContent = () => ({ active: true, retained: true });
const stateKeyOrder = [fileEventId2, fileEventId];
@@ -287,23 +288,21 @@ describe("MSC3089Branch", () => {
const createFn = jest
.fn()
.mockImplementation(
(name: string, contents: ArrayBuffer, info: Partial<IEncryptedFile>, addl: IContent) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: fileEventId,
},
});
.mockImplementation((name: string, contents: ArrayBuffer, info: Partial<EncryptedFile>, addl: IContent) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: fileEventId,
},
});
return Promise.resolve({ event_id: fileEventId2 });
},
);
return Promise.resolve({ event_id: fileEventId2 });
});
directory.createFile = createFn;
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
+10 -8
View File
@@ -25,6 +25,8 @@ import {
} from "../../../src/models/MSC3089TreeSpace";
import { DEFAULT_ALPHABET } from "../../../src/utils";
import { MatrixError } from "../../../src/http-api";
import { KnownMembership } from "../../../src/@types/membership";
import { EncryptedFile } from "../../../src/@types/media";
describe("MSC3089TreeSpace", () => {
let client: MatrixClient;
@@ -458,14 +460,14 @@ describe("MSC3089TreeSpace", () => {
expect(stateKey).toBeUndefined();
return [
// Partial implementations
{ getContent: () => ({ membership: "join" }), getStateKey: () => joinMemberId },
{ getContent: () => ({ membership: "knock" }), getStateKey: () => knockMemberId },
{ getContent: () => ({ membership: "invite" }), getStateKey: () => inviteMemberId },
{ getContent: () => ({ membership: "leave" }), getStateKey: () => leaveMemberId },
{ getContent: () => ({ membership: "ban" }), getStateKey: () => banMemberId },
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => joinMemberId },
{ getContent: () => ({ membership: KnownMembership.Knock }), getStateKey: () => knockMemberId },
{ getContent: () => ({ membership: KnownMembership.Invite }), getStateKey: () => inviteMemberId },
{ getContent: () => ({ membership: KnownMembership.Leave }), getStateKey: () => leaveMemberId },
{ getContent: () => ({ membership: KnownMembership.Ban }), getStateKey: () => banMemberId },
// ensure we don't kick ourselves
{ getContent: () => ({ membership: "join" }), getStateKey: () => selfUserId },
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => selfUserId },
];
},
};
@@ -946,7 +948,7 @@ describe("MSC3089TreeSpace", () => {
const fileInfo = {
mimetype: "text/plain",
// other fields as required by encryption, but ignored here
};
} as unknown as EncryptedFile;
const fileEventId = "$file";
const fileName = "My File.txt";
const fileContents = "This is a test file";
@@ -1006,7 +1008,7 @@ describe("MSC3089TreeSpace", () => {
const fileInfo = {
mimetype: "text/plain",
// other fields as required by encryption, but ignored here
};
} as unknown as EncryptedFile;
const fileEventId = "$file";
const fileName = "My File.txt";
const fileContents = "This is a test file";
+43 -50
View File
@@ -27,6 +27,8 @@ import {
THREAD_RELATION_TYPE,
TweakName,
} from "../../../src";
import { DecryptionFailureCode } from "../../../src/crypto-api";
import { DecryptionError } from "../../../src/common-crypto/CryptoBackend";
describe("MatrixEvent", () => {
it("should create copies of itself", () => {
@@ -360,27 +362,62 @@ describe("MatrixEvent", () => {
});
});
it("should report decryption errors", async () => {
it("should report unknown decryption errors", async () => {
const decryptionListener = jest.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new Error("test error");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
decryptEvent: jest.fn().mockRejectedValue(testError),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR);
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: Error: test error **",
});
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
});
it("should report known decryption errors", async () => {
const decryptionListener = jest.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(testError),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.decryptionFailureReason).toEqual(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: DecryptionError: uisi **",
});
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
});
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
const crypto = {
decryptEvent: jest
.fn()
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
.mockRejectedValue(
new DecryptionError(
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
"The sender has disabled encrypting to unverified devices.",
),
),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
@@ -423,6 +460,8 @@ describe("MatrixEvent", () => {
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual("m.room.message");
expect(encryptedEvent.isDecryptionFailure()).toBe(false);
expect(encryptedEvent.decryptionFailureReason).toBe(null);
});
});
@@ -469,52 +508,6 @@ describe("MatrixEvent", () => {
default: false,
enabled: true,
} as IAnnotatedPushRule;
describe("setPushActions()", () => {
it("sets actions on event", () => {
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(actions);
expect(event.getPushActions()).toBe(actions);
});
it("sets actions to undefined", () => {
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(null);
// undefined is set on state
expect(event.getPushDetails().actions).toBe(undefined);
// but pushActions getter returns null when falsy
expect(event.getPushActions()).toBe(null);
});
it("clears existing push rule", () => {
const prevActions = { notify: true, tweaks: { highlight: true } };
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
});
});
describe("setPushDetails()", () => {
it("sets actions and rule on event", () => {
@@ -543,7 +536,7 @@ describe("MatrixEvent", () => {
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
event.setPushDetails(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
+35 -16
View File
@@ -20,10 +20,12 @@ limitations under the License.
import fetchMock from "fetch-mock-jest";
import { mocked } from "jest-mock";
import jwtDecode from "jwt-decode";
import { jwtDecode } from "jwt-decode";
import { Crypto } from "@peculiar/webcrypto";
import { getRandomValues } from "node:crypto";
import { TextEncoder } from "node:util";
import { Method } from "../../../src";
import * as crypto from "../../../src/crypto/crypto";
import { logger } from "../../../src/logger";
import {
completeAuthorizationCodeGrant,
@@ -36,13 +38,10 @@ import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-uti
jest.mock("jwt-decode");
// save for resetting mocks
const realSubtleCrypto = crypto.subtleCrypto;
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint;
const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint;
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
const clientId = "xyz789";
const baseUrl = "https://test.com";
@@ -53,12 +52,22 @@ describe("oidc authorization", () => {
jest.spyOn(logger, "warn");
jest.setSystemTime(now);
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
fetchMock.get(
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
global.TextEncoder = TextEncoder;
});
afterEach(() => {
// @ts-ignore reset any ugly mocking we did
crypto.subtleCrypto = realSubtleCrypto;
beforeEach(() => {
const webCrypto = new Crypto();
Object.defineProperty(window, "crypto", {
value: {
getRandomValues,
randomUUID: jest.fn().mockReturnValue("not-random-uuid"),
subtle: webCrypto.subtle,
},
});
});
it("should generate authorization params", () => {
@@ -80,11 +89,8 @@ describe("oidc authorization", () => {
describe("generateAuthorizationUrl()", () => {
it("should generate url with correct parameters", async () => {
// test the no crypto case here
// @ts-ignore mocking
crypto.subtleCrypto = undefined;
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
authorizationParams.codeVerifier = "test-code-verifier";
const authUrl = new URL(
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
);
@@ -96,6 +102,18 @@ describe("oidc authorization", () => {
expect(authUrl.searchParams.get("scope")).toEqual(authorizationParams.scope);
expect(authUrl.searchParams.get("state")).toEqual(authorizationParams.state);
expect(authUrl.searchParams.get("nonce")).toEqual(authorizationParams.nonce);
expect(authUrl.searchParams.get("code_challenge")).toEqual("0FLIKahrX7kqxncwhV5WD82lu_wi5GA8FsRSLubaOpU");
});
it("should log a warning if crypto is not available", async () => {
// test the no crypto case here
// @ts-ignore mocking
globalThis.crypto.subtle = undefined;
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
const authUrl = new URL(
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
);
// crypto not available, plain text code_challenge is used
expect(authUrl.searchParams.get("code_challenge")).toEqual(authorizationParams.codeVerifier);
@@ -165,7 +183,8 @@ describe("oidc authorization", () => {
token_type: "Bearer",
access_token: "test_access_token",
refresh_token: "test_refresh_token",
id_token: "valid.id.token",
id_token:
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJleHAiOjE3MDgzNTY3NjcsInN1YiI6IjAxSFBQMkZTQllERTlQOUVNTThERDdXWkhSIiwiYXVkIjoiMDFIUTBXSDUyV0paV1JSU1k5V0VFUTVUMlEiLCJub25jZSI6ImhScEI2cGtFMDYiLCJhdXRoX3RpbWUiOjE3MDc5OTAzMTIsImlhdCI6MTcwODM1MzE2NywiYXRfaGFzaCI6Il9HSEU4cDhocHFnMW1ac041YUlycVEiLCJpc3MiOiJodHRwczovL2F1dGgtb2lkYy5sYWIuZWxlbWVudC5kZXYvIiwiY19oYXNoIjoib2hJRmNuaUZWd3pGSzVJdXlsX1RlQSJ9.SGUG78dCC3sTWgQBDTicKwamKiPpb6REiz79CM2ml_kVJCoS7gT0TlztC4h25FKi3c9aB3XCVn9J8UzvJgvG8Rt_oS--FIuhK6oRm7NdcN0bCkbG7iZEWGxx-kQnifcCFHyZ6T1CxR8X00Uvc6_lRfBZVlTyuuQaJ_PHiiKMlV93FbxvQUIq6FTkQP2Z56p4JIXIzjOONzA91skTqQGycl5f9Vhp6cqXFzl6ARK30M7A-8UI5qCxClUJ7kD9KgN5YZ7uivLp1x01WBnik2DXH0eSwXcTX2WLkYtMXgMxylJhIiO586apIC5nr7sfip-Y_4PgBlSjRRgrmOGC-VUFCA",
expires_in: 300,
};
+58 -22
View File
@@ -17,39 +17,48 @@ limitations under the License.
import fetchMockJest from "fetch-mock-jest";
import { OidcError } from "../../../src/oidc/error";
import { registerOidcClient } from "../../../src/oidc/register";
import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
describe("registerOidcClient()", () => {
const issuer = "https://auth.com/";
const registrationEndpoint = "https://auth.com/register";
const clientName = "Element";
const baseUrl = "https://just.testing";
const metadata: OidcRegistrationClientMetadata = {
clientUri: baseUrl,
redirectUris: [baseUrl],
clientName,
applicationType: "web",
tosUri: "http://tos-uri",
policyUri: "http://policy-uri",
contacts: ["admin@example.com"],
};
const dynamicClientId = "xyz789";
const delegatedAuthConfig = {
issuer,
registrationEndpoint,
authorizationEndpoint: issuer + "auth",
tokenEndpoint: issuer + "token",
};
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
beforeEach(() => {
fetchMockJest.mockClear();
fetchMockJest.resetBehavior();
});
it("should make correct request to register client", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(registrationEndpoint, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registrationEndpoint!,
expect.objectContaining({
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
method: "POST",
}),
);
expect(JSON.parse(fetchMockJest.mock.calls[0][1]!.body as string)).toEqual(
expect.objectContaining({
client_name: clientName,
client_uri: baseUrl,
response_types: ["code"],
@@ -59,26 +68,53 @@ describe("registerOidcClient()", () => {
token_endpoint_auth_method: "none",
application_type: "web",
}),
});
);
});
it("should throw when registration request fails", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 500,
});
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
OidcError.DynamicRegistrationFailed,
);
});
it("should throw when registration response is invalid", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
// no clientId in response
body: "{}",
});
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
OidcError.DynamicRegistrationInvalid,
);
});
it("should throw when required endpoints are unavailable", async () => {
await expect(() =>
registerOidcClient(
{
...delegatedAuthConfig,
registrationEndpoint: undefined,
},
metadata,
),
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
});
it("should throw when required scopes are unavailable", async () => {
await expect(() =>
registerOidcClient(
{
...delegatedAuthConfig,
metadata: {
...delegatedAuthConfig.metadata,
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
},
},
metadata,
),
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
});
});
+16 -16
View File
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
keys: [],
});
fetchMock.post(config.metadata.token_endpoint, {
fetchMock.post(config.tokenEndpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -88,7 +88,7 @@ describe("OidcTokenRefresher", () => {
},
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.oidcClientReady).rejects.toThrow();
expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OIDC client.",
@@ -98,7 +98,7 @@ describe("OidcTokenRefresher", () => {
});
it("initialises oidc client", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// @ts-ignore peek at private property to see we initialised the client correctly
@@ -114,19 +114,19 @@ describe("OidcTokenRefresher", () => {
describe("doRefreshAccessToken()", () => {
it("should throw when oidcClient has not been initialised", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
"Cannot get new token before OIDC client is initialised.",
);
});
it("should refresh the tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
const result = await refresher.doRefreshAccessToken("refresh-token");
expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, {
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
method: "POST",
});
@@ -137,7 +137,7 @@ describe("OidcTokenRefresher", () => {
});
it("should persist the new tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// spy on our stub
jest.spyOn(refresher, "persistTokens");
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
it("should only have one inflight refresh request at once", async () => {
fetchMock
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -175,7 +175,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
const result2 = await first;
// only one call to token endpoint
expect(fetchMock).toHaveFetchedTimes(1, config.metadata.token_endpoint);
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
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.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 503,
headers: {
@@ -218,7 +218,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
// make sure inflight request is cleared after a failure
fetchMock
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 503,
headers: {
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -249,7 +249,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();

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