Compare commits

..

217 Commits

Author SHA1 Message Date
RiotRobot c4048d985d v34.10.0 2024-11-05 13:49:13 +00:00
RiotRobot a8f8a9c14d v34.10.0-rc.0 2024-10-29 12:55:48 +00:00
Florian Duros 0e2f73d7a7 Deprecate CreateSecretStorageOpts.keyBackupInfo used in CryptoApi.bootstrapSecretStorage. (#4474) 2024-10-28 12:00:39 +00:00
Hugh Nimmo-Smith 31aeb3044f Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() (#4380)
* Add CryptoApi. encryptToDeviceMessages

Deprecate Crypto. encryptAndSendToDevices and MatrixClient. encryptAndSendToDevices

* Overload MatrixClient. encryptAndSendToDevices instead of deprecating

* Revert "Overload MatrixClient. encryptAndSendToDevices instead of deprecating"

This reverts commit 6a0d8e26385c34d40e8c2ed1e34cb5119c12456c.

* Feedback from code review

* Use temporary pre-release build of @matrix-org/matrix-sdk-crypto-wasm

* Deduplicate user IDs

* Test for RustCrypto implementation

* Use ensureSessionsForUsers()

* Encrypt to-device messages in parallel

* Use release version of matrix-sdk-crypto-wasm

* Upgrade matrix-sdk-crypto-wasm to v8

* Sync with develop

* Add test for olmlib CryptoApi

* Fix link

* Feedback from review

* Move libolm implementation to better place in file

* FIx doc

* Integration test

* Make sure test device is known to client

* Feedback from review
2024-10-28 11:32:17 +00:00
Hugh Nimmo-Smith 0a29063bc9 Do not rotate MatrixRTC media encryption key when a new member joins a session (#4472)
* Do not rotate MatrixRTC media encryption key when a new member joins a call

This change reverts https://github.com/matrix-org/matrix-js-sdk/pull/4422.

Instead, the rotation when a new member joins will be reintroduced as part of supporting to-device based MatrixRTC encryption key distribution.

* Improve function name
2024-10-25 13:32:44 +00:00
Richard van der Hoff 3cc3bd0728 Avoid <sender>|<session> notation in log messages (#4473)
We used to use the notation `<sender key>|<megolm session id>` fairly widely in
log messages, but since the transition to rust crypto, it's unusual and now
somewhat confusing. Make the log messages more explicit.
2024-10-25 12:48:44 +00:00
Michael Telatynski f891fe4423 Fix gitflow workflow not handling edge case (#4463)
of not needing to reset package to `#develop`

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-25 11:15:00 +00:00
Andrew Ferrazzutti b99ff83785 Refactor/simplify Promises in MatrixRTCSession (#4466)
* Refactor/simplify Promises in MatrixRTCSession

* Update src/matrixrtc/MatrixRTCSession.ts

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

* Fix+document+test leaveRoomSession's return value

* Throw instead of using expect in teardown

because lint rules forbid using expect outside of test functions

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2024-10-25 09:24:52 +00:00
Michael Telatynski 23c4c9fd8a Remove abandoned MSC3886, MSC3903, MSC3906 implementations (#4469)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-24 13:19:29 +00:00
RiotRobot 8b8ee91210 Merge branch 'master' into develop 2024-10-22 11:58:31 +00:00
RiotRobot d8c431f23e v34.9.0 2024-10-22 11:58:01 +00:00
Hugh Nimmo-Smith a6fb7530cb Organise MatrixRTCSession tests (#4453)
None of the tests themselves should have changed
2024-10-21 16:24:02 +00:00
Florian Duros b5c3f15a67 Deprecate MatrixClient.getDehydratedDevice (#4467) 2024-10-21 13:21:05 +00:00
renovate[bot] 91f6f0f9c5 Update typescript-eslint monorepo to v8 (major) (#4388)
* Update typescript-eslint monorepo to v8

* Migrate to stylistic

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-21 13:17:20 +00:00
renovate[bot] 88cf5eb769 Update actions/checkout digest to eef6144 (#4464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 13:54:33 +00:00
Andrew Ferrazzutti 13a967ae8f Prepare delayed call leave events more reliably (#4447)
* Prepare delayed call leave events more reliably

- Try sending call join after preparing delayed leave
- On leave, send delayed leave instead of a new event

* Don't rely on errcodes for retry logic

because they are unavailable in widget mode

* Make arrow method readonly

SonarCloud rule typescript:S2933

* Test coverage for restarting delayed call leave

* Remove unneeded unstable_features mock

It's unneeded because all affected methods are mocked
2024-10-17 17:41:18 +00:00
Michael Telatynski 66c80949e8 Pin GHA runner versions (#4461)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-17 09:04:19 +00:00
renovate[bot] 3d51e31da8 Update babel monorepo (#4454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 14:27:13 +00:00
renovate[bot] 86c190fa0d Update dependency @types/node to v18.19.55 (#4455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 14:21:56 +00:00
renovate[bot] e9a9280e3c Update all non-major dependencies (#4458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 13:34:51 +00:00
renovate[bot] fabdd6da64 Update dependency eslint-plugin-unicorn to v56 (#4459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 13:30:52 +00:00
renovate[bot] ab5d95102c Update dependency typescript to v5.6.3 (#4456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 12:51:13 +00:00
renovate[bot] b4556d6552 Update typedoc (#4457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 12:50:55 +00:00
renovate[bot] 6c22da9a96 Update dependency eslint to v8.57.1 (#4437)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-10-16 12:50:08 +00:00
renovate[bot] d29329bddb Replace dependency eslint-plugin-node with eslint-plugin-n ^14.0.0 (#4416)
* Replace dependency eslint-plugin-node with eslint-plugin-n ^14.0.0

* Update eslint config

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-10-16 12:49:22 +00:00
Michael Telatynski 68b06d7b60 Update workflows referencing matrix-react-sdk to use only element-web (#4451) 2024-10-16 13:48:21 +01:00
Andrew Ferrazzutti 5f599ee978 Fix DelayedEventInfo type (#4446)
* Fix DelayedEventInfo type

for MSC4140's GET /delayed_events

* Satisfy linter while avoiding unaligned indents

* Remove transaction_id from DelayedEventInfo

See matrix-org/matrix-spec-proposals@883e6b5d
2024-10-15 21:06:36 +00:00
Florian Duros aabd558bce Deprecate top level crypto event re-export (#4444) 2024-10-15 15:00:43 +00:00
Florian Duros 662b772c73 Add crypto events to crypto-api (#4443)
* Move used Crypto event into crypto api

* Use new crypto events in rust crypto

* Remove `WillUpdateDevices` event from CryptoApi

* Use new crypto events in old crypto events

* Compute type of CryptoEvent enum

* Rename CryptoEvent and CryptoEventHandlerMap as legacy

* - Rename `RustCryptoEvent` as `CryptoEvent`
- Declare `CryptoEventHandlerMap` into the crypto api

* Add `WillUpdateDevices` back to new crypto events to avoid circular imports between old crypto and the cryto api

* Extends old crypto handler map with the new crypto map

* Review fixes

* Add more explicit documentations
2024-10-15 14:38:33 +00:00
RiotRobot b240c44128 v34.9.0-rc.0 2024-10-15 14:31:37 +00:00
RiotRobot 5508993d79 Merge branch 'master' into develop 2024-10-15 10:53:49 +00:00
RiotRobot c9b43ab251 v34.8.0 2024-10-15 10:53:23 +00:00
David Baker 2fb1e659c8 Merge commit from fork
Remove insecure MatrixClient.sendSharedHistoryKeys method
2024-10-15 11:48:16 +01:00
RiotRobot b842192a34 Merge branch 'master' into develop 2024-10-08 12:23:01 +00:00
RiotRobot 868bbfcb31 v34.7.0 2024-10-08 12:22:28 +00:00
Saul 860161bdd2 Fix the rust crypto import in esm environments. (#4445)
* Configure babel to fix the rust import in esm environments.

* Add lockfile changes.

* Cleanup rust-crypto import and babel config.
2024-10-08 10:34:01 +00:00
Florian Duros 0c9d82e40a Deprecate the crypto events which are not used by the rust-crypto (#4442)
* Deprecate crypto event not used by the rust-crypto

* Deprecate `Crypto.WillUpdateDevices` for `Crypto.DevicesUpdated`
2024-10-07 12:26:56 +00:00
Richard van der Hoff 73ab7c342a Expose crypto-api as its own typedoc module (#4439)
Currently the crypto-api hierarchy is exposed only as a `Crypto` namespace
under the "matrix" entrypoint in the documentation.

This isn't really right: it's meant to be a separate entrypoint (in the same
way as `types`, `testing` and `utils` are). This PR fixes that problem.
2024-10-07 11:38:28 +00:00
Hugh Nimmo-Smith 3386c66b98 Fix MatrixRTC sender key wrapping (#4441) 2024-10-07 10:34:23 +00:00
Florian Duros da044820d7 Clean AES export and move back calculateKeyCheck to secret-storage.ts (#4440) 2024-10-03 13:20:56 +00:00
renovate[bot] 9f40f32b3e Update dependency @types/node to v18.19.54 (#4436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:37:25 +00:00
renovate[bot] f679942584 Update all non-major dependencies (#4438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:16:22 +00:00
RiotRobot c1b4f97372 v34.7.0-rc.0 2024-10-01 14:54:58 +00:00
Florian Duros 5f3b89990d Move out crypto/aes (#4431)
* Move `SecretEncryptedPayload` in `src/utils/@types`

* Move `encryptAES` to a dedicated file. Moved in a utils folder.

* Move `deriveKeys` to a dedicated file in order to share it

* Move `decryptAES` to a dedicated file. Moved in a utils folder.

* Move `calculateKeyCheck` to a dedicated file. Moved in a utils folder.

* Remove AES functions in `aes.ts` and export new ones for backward compatibility

* Update import to use new functions

* Add `src/utils` entrypoint in `README.md`

* - Rename `SecretEncryptedPayload` to `AESEncryptedSecretStoragePayload`.
- Move into `src/@types`

* Move `calculateKeyCheck` into `secret-storage.ts`.

* Move `deriveKeys` into `src/utils/internal` folder.

* - Rename `encryptAES` on `encryptAESSecretStorageItem`
- Change named export by default export

* - Rename `decryptAES` on `decryptAESSecretStorageItem`
- Change named export by default export

* Update documentation

* Update `decryptAESSecretStorageItem` doc

* Add lnk to spec for `calculateKeyCheck`

* Fix downstream tests
2024-10-01 13:52:59 +00:00
Richard van der Hoff 866fd6f4a3 Bump matrix-rust-sdk to 9.1.0 (#4435)
Mostly, to pick up https://github.com/matrix-org/matrix-rust-sdk/pull/3985
2024-09-30 16:46:33 +00:00
Valere 9ecb66e695 crypto: configure key sharing strategy based on DeviceIsolationMode (#4425)
* crypto: configure key sharing strategy based on deviceIsolationMode

fix eslint import error

cryptoMode was renamed to deviceIsolationMode

post rebase fix: Device Isolation mode name changes

* Fix outdated docs referring to old cryptomode

* code review: better comment for globalBlacklistUnverifiedDevices option

* RoomEncryptor: Use appropriate default for getBlacklistUnverifiedDevices

* do not provide a default value for DeviceIsolationMode for encryption

* Update src/rust-crypto/RoomEncryptor.ts

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-09-30 14:26:34 +00:00
Hugh Nimmo-Smith baa6d13506 RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() (#4427)
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys()

* Clarify comment

* Feedback from code review

* Update src/matrixrtc/MatrixRTCSession.ts

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

* Fix test

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-09-27 15:55:07 +00:00
Hugh Nimmo-Smith 2d6230f199 Rotate RTC key when a new member joins (#4422) 2024-09-27 15:54:48 +00:00
Tulir Asokan 825d85f18d Update media event content types to include captions (#4403)
Signed-off-by: Tulir Asokan <tulir@maunium.net>
2024-09-26 16:06:59 +00:00
Richard van der Hoff 1dcb7a6e75 Remove insecure MatrixClient.sendSharedHistoryKeys method
This method is impossible to use securely, and so is being removed. (It also
didn't work under Rust cryptography.)

In future, this functionality may be re-introduced in a safer way, but doing so
will probably require updates to the MSC.
2024-09-26 12:03:45 +01:00
Richard van der Hoff 823316b2ff Remove use of insecure sendSharedHistoryKeys in MSC3089 impl 2024-09-26 12:01:51 +01:00
renovate[bot] d56fa197d0 Update dependency typescript to v5.6.2 (#4420)
* Update dependency typescript to v5.6.2

* Fix TS errors

* Update minimal version of TS to `5.4.2` since the code is not compliant with an older version.

* Review fixes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
2024-09-26 08:23:38 +00:00
Michael Telatynski f7229bfff0 Update OIDC registration types to match latest MSC2966 state (#4432)
* Update OIDC registration types to match latest MSC2966 state

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

* Add comment

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-09-25 16:30:13 +00:00
Valere 538717c23e crypto: Replace cryptoMode with DeviceIsolationMode concept (#4429)
* crypto: Replace cryptoMode with DeviceIsolationMode concept

* use enum instead of string for the IsolationMode kind

* Code review - Cleaning, renaming

* review: unneeded @see in doc

* review: Rename IsolationMode with better names

* review: quick cleaning and doc
2024-09-25 13:33:02 +00:00
Richard van der Hoff 1a8ea3d685 Add CryptoApi.pinCurrentUserIdentity and UserIdentity.needsUserApproval (#4415)
* Implement `UserVerificationStatus.needsUserApproval`

Expose the `identityNeedsUserApproval` flag from the rust crypto crate.

* Add CryptoApi.pinCurrentUserIdentity

Expose `pinCurrentMasterKey` from the rust crypto api.

* Test data: add second cross-signing key for Bob

* Add tests for verification status
2024-09-24 16:38:18 +00:00
Florian Duros d0890d9450 Update sonarcloud action to use v3.3 matrix-org/sonarcloud-workflow-action (#4430) 2024-09-24 15:35:40 +00:00
RiotRobot 42ec17b977 Merge branch 'master' into develop 2024-09-24 12:37:47 +00:00
RiotRobot 82e9eefce6 v34.6.0 2024-09-24 12:37:19 +00:00
renovate[bot] f8208b1891 Update typedoc (#4419)
* Update typedoc

* Don't link a private method in tsdoc of a public method

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
2024-09-23 13:47:49 +00:00
Richard van der Hoff 092a59af66 Throw clearer error if createSecretStorageKey misbehaves (#4412)
https://github.com/element-hq/element-web/issues/27888 shows a slightly
mysterious error message, but the problem is `createSecretStorageKey`
returning the wrong thing.
2024-09-23 10:25:42 +00:00
renovate[bot] dbd7d26968 Update dependency @types/node to v18.19.50 (#4418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 15:26:50 +00:00
renovate[bot] 4fda9e8419 Update all non-major dependencies (#4417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 13:38:54 +00:00
RiotRobot a13e0389db v34.6.0-rc.0 2024-09-18 13:31:13 +00:00
Hubert Chathi dbb4828eda Add crypto mode setting for invisible crypto, and apply it to decrypting events (#4407)
Adds a global "crypto mode" setting to the crypto API (only works with Rust crypto), and changes the decryption settings based on that.
2024-09-18 12:53:07 +01:00
Hugh Nimmo-Smith 414ac9d8cc Don't share full key history for RTC per-participant encryption (#4406)
* Don't share full key history for RTC per-participant encryption

Also record stats for how many keys have been sent/received and age of those received

* Update src/matrixrtc/MatrixRTCSession.ts

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

* Add comment about why we track total age of events

---------

Co-authored-by: Robin <robin@robin.town>
2024-09-18 08:57:26 +00:00
Johannes Marbach 30058a4bdc Add ESLint rule to ensure .ts extensions on imports (#4409)
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-18 07:40:01 +00:00
Florian Duros fab9cab3df Move crypto/key_passphrase.ts to crypto-api/key-passphrase.ts (#4401)
* Move `crypto/key_passphrase.ts` to `crypto-api/key-passphrase.ts`

* Re-export `crypto-api/key-passphrase` into `crypto/key_passphrase.ts`

* Add doc

* Deprecate `MatrixClient.keyBackupKeyFromPassword`

* Move `keyFromAuthData` to `common-crypto/key-passphrase.ts`

* Fix faulty import

* Keep `keyFromPassphrase` in old crypto

* - Rename `deriveKey` into `deriveRecoveryKeyFromPassphrase`
- Call `deriveRecoveryKeyFromPassphrase` into `RustCrypto.createRecoveryKeyFromPassphrase` instead of using `keyFromPassphrase`

* Remove alternative in `keyBackupKeyFromPassword` deprecation.

* Add tests for `keyFromAuthData`

* Deprecate `keyFromAuthData`

* Review changes
2024-09-17 13:05:47 +00:00
Johannes Marbach 53b599f8fe Export membership types (#4405)
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-14 10:44:51 +00:00
Timo 17f6cc733e Fix redact sending over in embedded (widget) mode (#4398) 2024-09-13 16:07:44 +00:00
Johannes Marbach c8403f39aa Fix build failure in node.js example (#4394)
* Fix node.js example

Relates to: element-hq/element-web#26922
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Update examples/node/app.js

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

* Update examples/node/package.json

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

* Move imports to the top of the file

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-09-13 15:35:45 +00:00
Florian Duros 8cf5df73ee Move crypto/recoverykey.ts to crypto-api/recovery-key.ts (#4399)
* Move `crypto/recoverykey.ts` to `crypto-api/recovery-key.ts`

* Re-export `crypto-api/recovery-key` into `crypto/recoverykey`

* Add a bit of doc

* Deprecate `MatrixClient.isValidRecoveryKey` and `MatrixClient.keyBackupKeyFromRecoveryKey`

* Import `index.ts` directly

* Update `recovery-key.ts` doc

* Add tests for `decodeRecoveryKey`

* Move `recovery-key.spec.ts` file
2024-09-13 13:26:30 +00:00
Florian Duros febe27ddcc Replace matrix.org/matrix-react-sdk by element-hq/matrix-react-sdk (#4404) 2024-09-13 13:03:31 +00:00
Hubert Chathi 7987ce76ec Bump dependency on rust-sdk-crypto-wasm to v8.0.0 (#4396) 2024-09-13 10:22:33 +01:00
Richard van der Hoff 60cedf2fdb Improve documentation on {encrypt,decrypt}AES (#4397) 2024-09-11 14:13:12 +00:00
Robin ed44514974 Expose the event ID of a call membership (#4395)
This is in line with the other information we're already exposing, such as the event's sender and timestamp. We want this in order to play around with adding reactions to the membership event.
2024-09-10 20:15:07 +00:00
RiotRobot 9f8c1ee953 Merge branch 'master' into develop 2024-09-10 12:31:32 +00:00
RiotRobot 593a57fc2b v34.5.0 2024-09-10 12:30:54 +00:00
Will Hunt e8128d34a1 MSC4133 - Extended profiles (#4391)
* Add MSC4133 functionality.

* Add MSC4133 capability.

* Tidy

* Add tests for extended profiles.

* improve docs

* undefined

* Add a prefix function to reduce reptitiveness

* Add a docstring
2024-09-09 12:06:38 +00:00
Richard van der Hoff ba7bd06295 Element-R: Mark unsupported MatrixClient methods as deprecated (#4389)
Slap a `@deprecated` tag on all the MatrixClient methods which no longer work
with rust crypto.
2024-09-05 13:37:08 +00:00
Richard van der Hoff e4db6008b8 Minor improvements to logging in device verification (#4390)
A grab-bag of small logging improvements in the Rust crypto wrapper.
2024-09-05 10:22:54 +00:00
Johannes Marbach 52f35409ec Stabilise MSC4156 (#4381)
* Stabilise MSC4156

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

* Add deprecation comments

* Add minimum Matrix version

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-04 12:01:33 +00:00
renovate[bot] f50aab37c3 Update babel monorepo (#4387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:29:21 +00:00
renovate[bot] df0f817f83 Update dependency typedoc-plugin-mdn-links to v3.2.10 (#4386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 21:20:01 +00:00
renovate[bot] 7efa6352f8 Update shogo82148/actions-upload-release-asset digest to aac270e (#4385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 20:32:50 +00:00
renovate[bot] f74614705e Update mheap/github-action-required-labels digest to d25134c (#4384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 20:32:29 +00:00
renovate[bot] 169e8f8613 Update dependency @types/node to v18.19.47 (#4383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 15:22:51 +00:00
renovate[bot] f2f77bd1f7 Update all non-major dependencies (#4382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 13:51:09 +00:00
RiotRobot 5a1c70ad19 v34.5.0-rc.0 2024-09-03 12:31:16 +00:00
David Baker 27cb16ffe4 Merge pull request #4379 from matrix-org/renovate/matrix-org
Update dependency matrix-widget-api to v1.9.0
2024-08-30 17:05:06 +01:00
renovate[bot] 9be0b3e701 Update dependency matrix-widget-api to v1.9.0 2024-08-30 15:54:06 +00:00
Johannes Marbach 05ba27f36b Change imports for Node.js compatibility (#4377)
* Change imports for Node.js compatibility

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

* Run prettier

* Run prettier (again)

* Add comment

* Update babel.config.cjs

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-08-29 15:56:32 +00:00
David Baker e6acfdf275 Rename merge release notes script to cjs (#4361)
and update the corresponding workflow file
2024-08-28 10:40:16 +00:00
RiotRobot 2a6612c73a Merge branch 'master' into develop 2024-08-27 12:38:13 +00:00
RiotRobot 2f8b05b0da v34.4.0 2024-08-27 12:37:46 +00:00
Hugh Nimmo-Smith fe984ede6e Use prefixed loggers for MatrixRTC (#4378) 2024-08-23 19:04:22 +00:00
Richard van der Hoff 3f74b9a0cc Deprecate unused CryptoCallbacks (#4376) 2024-08-23 14:00:27 +00:00
David Baker 802b996b10 Set web team as codeowner for the scripts dir (#4354)
The scripts in here are used in the release, and from the develop
branch too (because it's the main branch and github actions does this)
so it's critical for the release process.
2024-08-23 12:46:27 +00:00
renovate[bot] 8d44f9d665 Update dependency eslint-plugin-unicorn to v55 (#4371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 12:32:26 +00:00
renovate[bot] a72a1b294a Update dependency eslint-plugin-jsdoc to v50 (#4370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 11:22:35 +00:00
renovate[bot] ab5f32f984 Update typedoc (#4367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 10:46:01 +00:00
renovate[bot] 6a21d812ab Update typescript-eslint monorepo to v7.18.0 (#4368)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 10:41:47 +00:00
Richard van der Hoff ee94e93354 set entrypoints to use ./lib rather than ./src (#4357)
Currently, we replace the entrypoints in package.json during the release
cycle. I think. historically, this was done to make matrix-react-sdk and
element-web development easier, but neither of those projects actually use
these entrypoints (instead they import from `src`).

Accordingly, I think the switcheroo is unnecessary; furthermore it causes a
whole bunch of confusion by making the development environment different from
the release environment, and it complicates our CI and release process.

In short, the switcheroo has to die.
2024-08-21 18:31:05 +00:00
renovate[bot] 31c4786a96 Update dependency fetch-mock to v11 (#4369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 16:14:39 +00:00
renovate[bot] 17b6e59819 Update shogo82148/actions-upload-release-asset digest to 6d4fd50 (#4366)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 15:47:42 +00:00
renovate[bot] 42510022a1 Update dependency typescript to v5.5.4 (#4365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 15:08:24 +00:00
renovate[bot] 5f0978ac3f Update babel monorepo (#4363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 14:41:16 +00:00
renovate[bot] cd6787e0ac Update dependency @types/node to v18.19.44 (#4364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 14:22:08 +00:00
renovate[bot] 03baa3e358 Update all non-major dependencies (#4362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 13:51:57 +00:00
RiotRobot 658563e2a7 v34.4.0-rc.1 2024-08-21 12:56:47 +00:00
David Baker 26d3033b17 Merge pull request #4358 from matrix-org/backport-4356-to-staging
[Backport staging] Rename slowReporter to reflect that it is CommonJS
2024-08-21 13:01:49 +01:00
David Baker a4bd7dc7d7 Merge branch 'staging' into backport-4356-to-staging 2024-08-21 12:53:16 +01:00
David Baker 1f48544b38 Merge pull request #4359 from matrix-org/dbkr/backport_4355
Move `type:module` declaration into package.json. (Backport 4355)
2024-08-21 12:46:40 +01:00
Richard van der Hoff 7ef4062f59 Move type:module declaration into package.json. (#4355)
* Rename `switch_package_to_release.js` to `.cjs`

Slightly surprisingly, the symlink is enough to make `node
switch_package_to_release.js` work.

* Rename .eslintrc.js to .cjs

Again, declare this as commonjs

* Move `type:module` declaration into package.json.

matrix-js-sdk is built into ECMAScript modules, and we should declare it as
such. See https://nodejs.org/api/packages.html#type. Failure to do so causes
problems for javascript projects attempting to build against matrix-js-sdk: see https://github.com/matrix-org/matrix-js-sdk/issues/4347.

Previously, we did this as part of the package.json switcheroo, but that is
unnecessarily fragile.

matrix-react-sdk, element-web, etc are unaffected by this, because they use the
typescript files directly, by importing `matrix-js-sdk/src/...`.
2024-08-21 12:04:59 +01:00
Richard van der Hoff 968bc51a35 Rename slowReporter to reflect that it is CommonJS (#4356)
Fix tests failing as a result of
https://github.com/matrix-org/matrix-js-sdk/pull/4355. This wasn't detected in
CI because the slowReporter is only enabled when building against develop.

(cherry picked from commit d413f5042e)
2024-08-21 09:39:52 +00:00
Richard van der Hoff d413f5042e Rename slowReporter to reflect that it is CommonJS (#4356)
Fix tests failing as a result of
https://github.com/matrix-org/matrix-js-sdk/pull/4355. This wasn't detected in
CI because the slowReporter is only enabled when building against develop.
2024-08-20 20:48:27 +00:00
Richard van der Hoff b8e8b14375 Move type:module declaration into package.json. (#4355)
* Rename `switch_package_to_release.js` to `.cjs`

Slightly surprisingly, the symlink is enough to make `node
switch_package_to_release.js` work.

* Rename .eslintrc.js to .cjs

Again, declare this as commonjs

* Move `type:module` declaration into package.json.

matrix-js-sdk is built into ECMAScript modules, and we should declare it as
such. See https://nodejs.org/api/packages.html#type. Failure to do so causes
problems for javascript projects attempting to build against matrix-js-sdk: see https://github.com/matrix-org/matrix-js-sdk/issues/4347.

Previously, we did this as part of the package.json switcheroo, but that is
unnecessarily fragile.

matrix-react-sdk, element-web, etc are unaffected by this, because they use the
typescript files directly, by importing `matrix-js-sdk/src/...`.
2024-08-20 16:23:03 +00:00
RiotRobot 43e58871de v34.4.0-rc.0 2024-08-20 13:40:49 +00:00
Richard van der Hoff 2544c14032 Unrevert prerelease fix, and fix release error (#4353)
* Reapply "Add "type" = "module" to ensure it is present (#4350)" (#4352)

This reverts commit 8214fd7156.

* Mark prettier config file as CommonJS

I *think* this will fix a problem with the release process in which we saw an
error:

```
Error:  Invalid configuration for file "/home/runner/work/matrix-js-sdk/matrix-js-sdk/package.json":
Error:  module is not defined in ES module scope
Error:  This file is being treated as an ES module because it has a '.js' file extension and '/home/runner/work/matrix-js-sdk/matrix-js-sdk/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
```
2024-08-20 13:16:44 +00:00
RiotRobot 8d19782c57 Merge branch 'master' into develop 2024-08-20 11:30:12 +00:00
RiotRobot 340bbe1a8f v34.3.1 2024-08-20 11:29:48 +00:00
David Baker 8214fd7156 Revert "Add "type" = "module" to ensure it is present (#4350)" (#4352)
This reverts commit d6080398db.
2024-08-20 11:10:15 +00:00
David Baker a0efed8b88 Merge commit from fork
Detect cycles when looking for predecessor rooms
2024-08-20 11:27:28 +01:00
Hugh Nimmo-Smith c408c0d1d5 Retry event decryption failures on first failure (#4346)
* Retry event decryption failures on first failure

* Suggestion from code review

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

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-08-19 13:35:45 +00:00
BLCK d6080398db Add "type" = "module" to ensure it is present (#4350) 2024-08-15 14:50:05 +00:00
Hugh Nimmo-Smith 467908703b Don't attempt to adjust for clock skews when calculating group call membership expiry (#4340)
* Use origin server timestamp for calculating group call membership expiry

* Fix tests

* Docs

* Refactor comments to reflect that the logic hasn't changed

* Make comment maintainable

* Fix up merge

* Fix test
2024-08-15 09:11:06 +00:00
Hugh Nimmo-Smith 87eddaf51a Handle MatrixRTC encryption keys arriving out of order (#4345)
* Handle MatrixRTC encryption keys arriving out of order

* Apply suggestions from code review

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

* Suggestion from code review

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-08-15 07:58:36 +00:00
Hugh Nimmo-Smith c65ef03567 Resend MatrixRTC encryption keys if a membership has changed (#4343)
* Resend MatrixRTC encryption keys if a membership has changed

* JSDoc

* Update src/matrixrtc/MatrixRTCSession.ts

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

* Add note about using Set. symmetricDifference() when available

* Always store latest fingerprints

Should reduce unnecessary retransmits

* Refactor

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-08-14 14:08:52 +00:00
RiotRobot 78cbf7cd28 Resetting package fields for development 2024-08-13 12:06:18 +00:00
RiotRobot dc7c1a4fef Merge branch 'master' into develop 2024-08-13 12:06:17 +00:00
RiotRobot 1ae0c2f3ee v34.3.0 2024-08-13 12:05:45 +00:00
Hugh Nimmo-Smith affaa95fb4 delayBeforeuse => delayBeforeUse for consistency (#4344) 2024-08-09 14:34:13 +00:00
Andrew Ferrazzutti 9176d3a671 Use non-legacy calls if any are found (#4337)
Akin to how legacy call events should be sent in rooms where there is
any ongoing legacy call, send non-legacy events in rooms where there are
only non-legacy calls; else fall back to the config preference.
2024-08-07 15:44:44 +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
David Baker e0ef467d7d break instead of return 2024-07-29 13:55:08 +01: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
David Baker 79299891fd Detect cycles when looking for predecessor rooms 2024-07-29 11:14:58 +01: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
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
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
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
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
291 changed files with 9566 additions and 7740 deletions
+11 -5
View File
@@ -1,5 +1,5 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc"],
plugins: ["matrix-org", "import", "jsdoc", "n"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
@@ -108,10 +108,8 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
// We do this sometimes to brand interfaces
"@typescript-eslint/no-empty-object-type": "off",
"quotes": "off",
// We use a `logger` intermediary module
@@ -128,6 +126,14 @@ module.exports = {
// These need a bit more work before we can enable
// "jsdoc/check-param-names": "error",
// "jsdoc/check-indentation": "error",
// Ensure .ts extension on imports outside of tests
"n/file-extension-in-import": [
"error",
"always",
{
tryExtensions: [".ts"],
},
],
},
},
{
+1
View File
@@ -2,6 +2,7 @@
/.github/workflows/** @matrix-org/element-web-team
/package.json @matrix-org/element-web-team
/yarn.lock @matrix-org/element-web-team
/scripts/** @matrix-org/element-web-team
/src/webrtc @matrix-org/element-call-reviewers
/src/matrixrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # 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@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # 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 -1
View File
@@ -10,7 +10,7 @@ on:
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
netlify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@v4
@@ -21,14 +21,13 @@ concurrency:
jobs:
playwright:
name: Playwright
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
permissions:
actions: read
issues: read
pull-requests: read
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
# 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' }}
+1 -3
View File
@@ -14,10 +14,8 @@ jobs:
include:
- repo: element-hq/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
+5 -5
View File
@@ -12,9 +12,9 @@ concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || gi
jobs:
changelog:
name: Preview Changelog
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -29,7 +29,7 @@ jobs:
prevent-blocked:
name: Prevent Blocked
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
pull-requests: read
steps:
@@ -42,7 +42,7 @@ jobs:
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: github.event.action == 'opened'
steps:
- name: Check membership
@@ -69,7 +69,7 @@ jobs:
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
@@ -10,7 +10,7 @@ on:
concurrency: release-drafter-action
jobs:
draft:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
@@ -56,7 +56,7 @@ jobs:
script: |
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
const { owner, repo } = context.repo;
const script = require("./.action-repo/scripts/release/merge-release-notes.js");
const script = require("./.action-repo/scripts/release/merge-release-notes.cjs");
let deps = [];
if (DEPENDENCY.includes("/")) {
+5 -5
View File
@@ -15,7 +15,7 @@ on:
concurrency: ${{ github.workflow }}
jobs:
merge:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@@ -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"
@@ -60,26 +61,25 @@ jobs:
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
echo "Current $PACKAGE version is $CURRENT_VERSION"
if [ "$CURRENT_VERSION" == "null" ]
if [[ "$CURRENT_VERSION" == "null" ]]
then
echo "Unable to find $PACKAGE in package.json"
exit 1
fi
if [ "$CURRENT_VERSION" == "develop" ]
if [[ "$CURRENT_VERSION" == *"#develop" ]]
then
echo "Not updating dependency $PACKAGE"
continue
fi
echo "Resetting $1 to develop branch..."
echo "Resetting $PACKAGE to develop branch..."
yarn add "github:matrix-org/$PACKAGE#develop"
git add -u
git commit -m "Reset $PACKAGE back to develop branch"
done <<< "$DEPENDENCIES"
env:
DEPENDENCIES: ${{ inputs.dependencies }}
FINAL: ${{ inputs.final }}
- name: Push changes
run: git push origin develop
+9 -7
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
@@ -45,7 +41,7 @@ on:
jobs:
release:
name: Release
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
environment: Release
steps:
- name: Load GPG key
@@ -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"
@@ -279,7 +276,7 @@ jobs:
post-release:
name: Post release steps
needs: release
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- id: repository
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
@@ -307,7 +304,7 @@ jobs:
bump-downstreams:
name: Update npm dependency in downstream projects
needs: npm
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: inputs.downstreams
strategy:
matrix:
@@ -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 }}
+2 -1
View File
@@ -11,7 +11,7 @@ on:
jobs:
npm:
name: Publish to npm
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
id: ${{ steps.npm-publish.outputs.id }}
steps:
@@ -25,6 +25,7 @@ jobs:
with:
cache: "yarn"
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
+4 -3
View File
@@ -28,13 +28,13 @@ jobs:
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
downstreams: '["matrix-org/matrix-react-sdk", "element-hq/element-web"]'
downstreams: '["element-hq/element-web"]'
docs:
name: Publish Documentation
needs: release
if: inputs.docs
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
@@ -43,6 +43,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
@@ -59,7 +60,7 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: docs
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
+3 -3
View File
@@ -14,7 +14,7 @@ on:
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
jobs:
sonarqube:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'merge_group'
@@ -30,7 +30,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
@@ -63,7 +63,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v3.2
uses: matrix-org/sonarcloud-workflow-action@v3.3
# 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:
+11 -6
View File
@@ -11,13 +11,14 @@ concurrency:
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
@@ -37,13 +38,14 @@ jobs:
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
@@ -53,13 +55,14 @@ jobs:
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
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"
@@ -69,19 +72,20 @@ jobs:
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors"
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@v4
@@ -93,13 +97,14 @@ jobs:
analyse_dead_code:
name: "Analyse Dead Code"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
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"
+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 }}
+19 -9
View File
@@ -13,12 +13,12 @@ env:
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
matrix:
specs: [integ, unit]
node: [18, "lts/*", 21]
node: ["lts/*", 22]
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -63,10 +63,20 @@ jobs:
coverage
!coverage/lcov-report
matrix-react-sdk:
name: Downstream test matrix-react-sdk
# Dummy completion job to simplify branch protections
jest-complete:
name: Jest tests
needs: jest
if: always()
runs-on: ubuntu-24.04
steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1
element-web:
name: Downstream test element-web
if: github.event_name == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
uses: element-hq/element-web/.github/workflows/tests.yml@develop
with:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
@@ -81,7 +91,7 @@ jobs:
# 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
runs-on: ubuntu-24.04
if: always()
needs:
- complement-crypto
@@ -93,10 +103,10 @@ jobs:
# and skip sonarcloud coverage within merge queues
downstream:
name: Downstream tests
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
if: always()
needs:
- matrix-react-sdk
- element-web
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
@@ -109,5 +119,5 @@ jobs:
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
run: exit 1
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
automate-project-columns-next:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/add-to-project@main
with:
View File
+136
View File
@@ -1,3 +1,139 @@
Changes in [34.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.10.0) (2024-11-05)
====================================================================================================
## 🦖 Deprecations
* Deprecate `CreateSecretStorageOpts.keyBackupInfo` used in `CryptoApi.bootstrapSecretStorage.` ([#4474](https://github.com/matrix-org/matrix-js-sdk/pull/4474)). Contributed by @florianduros.
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
* Remove abandoned MSC3886, MSC3903, MSC3906 experimental implementations ([#4469](https://github.com/matrix-org/matrix-js-sdk/pull/4469)). Contributed by @t3chguy.
* Deprecate `MatrixClient.getDehydratedDevice` ([#4467](https://github.com/matrix-org/matrix-js-sdk/pull/4467)). Contributed by @florianduros.
* Deprecate top level crypto events re-export ([#4444](https://github.com/matrix-org/matrix-js-sdk/pull/4444)). Contributed by @florianduros.
## ✨ Features
* Add CryptoApi.encryptToDeviceMessages() and deprecate Crypto.encryptAndSendToDevices() ([#4380](https://github.com/matrix-org/matrix-js-sdk/pull/4380)). Contributed by @hughns.
* Do not rotate MatrixRTC media encryption key when a new member joins a session ([#4472](https://github.com/matrix-org/matrix-js-sdk/pull/4472)). Contributed by @hughns.
* Avoid `<sender>|<session>` notation in log messages ([#4473](https://github.com/matrix-org/matrix-js-sdk/pull/4473)). Contributed by @richvdh.
* Refactor/simplify Promises in MatrixRTCSession ([#4466](https://github.com/matrix-org/matrix-js-sdk/pull/4466)). Contributed by @AndrewFerr.
* Prepare delayed call leave events more reliably ([#4447](https://github.com/matrix-org/matrix-js-sdk/pull/4447)). Contributed by @AndrewFerr.
## 🐛 Bug Fixes
* Fix DelayedEventInfo type ([#4446](https://github.com/matrix-org/matrix-js-sdk/pull/4446)). Contributed by @AndrewFerr.
Changes in [34.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.9.0) (2024-10-22)
==================================================================================================
## 🦖 Deprecations
* Deprecate the crypto events which are not used by the rust-crypto ([#4442](https://github.com/matrix-org/matrix-js-sdk/pull/4442)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix the rust crypto import in esm environments. ([#4445](https://github.com/matrix-org/matrix-js-sdk/pull/4445)). Contributed by @saul-jb.
* Fix MatrixRTC sender key wrapping ([#4441](https://github.com/matrix-org/matrix-js-sdk/pull/4441)). Contributed by @hughns.
Changes in [34.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.8.0) (2024-10-15)
==================================================================================================
This release removes insecure functionality, resolving CVE-2024-47080 / GHSA-4jf8-g8wp-cx7c.
Changes in [34.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.7.0) (2024-10-08)
==================================================================================================
## 🦖 Deprecations
* RTCSession cleanup: deprecate getKeysForParticipant() and getEncryption(); add emitEncryptionKeys() ([#4427](https://github.com/matrix-org/matrix-js-sdk/pull/4427)). Contributed by @hughns.
## ✨ Features
* Bump matrix-rust-sdk to 9.1.0 ([#4435](https://github.com/matrix-org/matrix-js-sdk/pull/4435)). Contributed by @richvdh.
* Rotate Matrix RTC media encryption key when a new member joins a call for Post Compromise Security ([#4422](https://github.com/matrix-org/matrix-js-sdk/pull/4422)). Contributed by @hughns.
* Update media event content types to include captions ([#4403](https://github.com/matrix-org/matrix-js-sdk/pull/4403)). Contributed by @tulir.
* Update OIDC registration types to match latest MSC2966 state ([#4432](https://github.com/matrix-org/matrix-js-sdk/pull/4432)). Contributed by @t3chguy.
* Add `CryptoApi.pinCurrentUserIdentity` and `UserIdentity.needsUserApproval` ([#4415](https://github.com/matrix-org/matrix-js-sdk/pull/4415)). Contributed by @richvdh.
Changes in [34.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.6.0) (2024-09-24)
==================================================================================================
## 🦖 Deprecations
* Element-R: Mark unsupported MatrixClient methods as deprecated ([#4389](https://github.com/matrix-org/matrix-js-sdk/pull/4389)). Contributed by @richvdh.
## ✨ Features
* Add crypto mode setting for invisible crypto, and apply it to decrypting events ([#4407](https://github.com/matrix-org/matrix-js-sdk/pull/4407)). Contributed by @uhoreg.
* Don't share full key history for RTC per-participant encryption ([#4406](https://github.com/matrix-org/matrix-js-sdk/pull/4406)). Contributed by @hughns.
* Export membership types ([#4405](https://github.com/matrix-org/matrix-js-sdk/pull/4405)). Contributed by @Johennes.
* Fix sending redacts in embedded (widget) mode ([#4398](https://github.com/matrix-org/matrix-js-sdk/pull/4398)). Contributed by @toger5.
* Expose the event ID of a call membership ([#4395](https://github.com/matrix-org/matrix-js-sdk/pull/4395)). Contributed by @robintown.
* MSC4133 - Extended profiles ([#4391](https://github.com/matrix-org/matrix-js-sdk/pull/4391)). Contributed by @Half-Shot.
Changes in [34.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.5.0) (2024-09-10)
==================================================================================================
## 🦖 Deprecations
* Deprecate unused callback hooks `CryptoCallbacks.onSecretRequested` and `CryptoCallbacks.getDehydrationKey` ([#4376](https://github.com/matrix-org/matrix-js-sdk/pull/4376)). Contributed by @richvdh.
Changes in [34.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.4.0) (2024-08-27)
==================================================================================================
## ✨ Features
* Use non-legacy calls if any are found ([#4337](https://github.com/matrix-org/matrix-js-sdk/pull/4337)). Contributed by @AndrewFerr.
## 🐛 Bug Fixes
* Retry event decryption failures on first failure ([#4346](https://github.com/matrix-org/matrix-js-sdk/pull/4346)). Contributed by @hughns.
* Ensure "type" = "module" ES declaration in pre-release.sh ([#4350](https://github.com/matrix-org/matrix-js-sdk/pull/4350)). Contributed by @BLCK-B.
* Handle MatrixRTC encryption keys arriving out of order ([#4345](https://github.com/matrix-org/matrix-js-sdk/pull/4345)). Contributed by @hughns.
* Resend MatrixRTC encryption keys if a membership has changed ([#4343](https://github.com/matrix-org/matrix-js-sdk/pull/4343)). Contributed by @hughns.
Changes in [34.3.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.3.1) (2024-08-20)
==================================================================================================
# Security
- Fixes for [CVE-2024-42369](https://nvd.nist.gov/vuln/detail/CVE-2024-42369) / [GHSA-vhr5-g3pm-49fm](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-vhr5-g3pm-49fm).
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
+2 -1
View File
@@ -39,7 +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.
[the Node.js terminal app](examples/node/README.md) for a more complex example.
To start the client:
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
| `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. |
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
## Examples
+52
View File
@@ -0,0 +1,52 @@
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",
{
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
// `.js` during transpilation, *except* when we are targetting Jest.
rewriteImportExtensions: process.env.NODE_ENV !== "test",
},
],
],
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",
[
"search-and-replace",
{
// Since rewriteImportExtensions doesn't work on dynamic imports (yet), we need to manually replace
// the dynamic rust-crypto import.
// (see https://github.com/babel/babel/issues/16750)
rules:
process.env.NODE_ENV !== "test"
? [
{
search: "./rust-crypto/index.ts",
replace: "./rust-crypto/index.js",
},
]
: [],
},
],
],
};
-26
View File
@@ -1,26 +0,0 @@
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",
],
};
+16 -11
View File
@@ -1,9 +1,15 @@
import clc from "cli-color";
import fs from "fs";
import readline from "readline";
import sdk, { ClientEvent, EventType, MsgType, RoomEvent } from "matrix-js-sdk";
import { KnownMembership } from "matrix-js-sdk/lib/@types/membership.js";
var myHomeServer = "http://localhost:8008";
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var sdk = require("matrix-js-sdk");
var clc = require("cli-color");
var matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
baseUrl: myHomeServer,
accessToken: myAccessToken,
userId: myUserId,
});
@@ -15,7 +21,6 @@ var numMessagesToShow = 20;
// Reading from stdin
var CLEAR_CONSOLE = "\x1B[2J";
var readline = require("readline");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
@@ -97,7 +102,7 @@ rl.on("line", function (line) {
})
.then(function (url) {
var content = {
msgtype: "m.file",
msgtype: MsgType.File,
body: filename,
url: JSON.parse(url).content_uri,
};
@@ -138,7 +143,7 @@ rl.on("line", function (line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("sync", function (state, prevState, data) {
matrixClient.on(ClientEvent.Sync, function (state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
@@ -149,7 +154,7 @@ matrixClient.on("sync", function (state, prevState, data) {
}
});
matrixClient.on("Room", function () {
matrixClient.on(ClientEvent.Room, function () {
setRoomList();
if (!viewingRoom) {
printRoomList();
@@ -158,7 +163,7 @@ matrixClient.on("Room", function () {
});
// print incoming messages.
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
@@ -305,7 +310,7 @@ function printRoomInfo(room) {
print(eTypeHeader + sendHeader + contentHeader);
print(new Array(100).join("-"));
eventMap.keys().forEach(function (eventType) {
if (eventType === "m.room.member") {
if (eventType === EventType.RoomMember) {
return;
} // use /members instead.
var eventEventMap = eventMap.get(eventType);
@@ -343,7 +348,7 @@ function printLine(event) {
name = name.slice(0, maxNameWidth - 1) + "\u2026";
}
if (event.getType() === "m.room.message") {
if (event.getType() === EventType.RoomMessage) {
body = event.getContent().body;
} else if (event.isState()) {
var stateName = event.getType();
@@ -394,4 +399,4 @@ function fixWidth(str, len) {
return str;
}
matrixClient.startClient(numMessagesToShow); // messages for each room.
matrixClient.startClient({ initialSyncLimit: numMessagesToShow });
+3 -4
View File
@@ -3,12 +3,11 @@
"version": "0.0.0",
"description": "",
"main": "app.js",
"scripts": {
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0",
"type": "module",
"dependencies": {
"cli-color": "^1.0.0"
"cli-color": "^1.0.0",
"matrix-js-sdk": "^34.5.0"
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ if (env["GITHUB_ACTIONS"] !== undefined) {
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
reporters.push("<rootDir>/spec/slowReporter.js");
reporters.push("<rootDir>/spec/slowReporter.cjs");
}
config.reporters = reporters;
}
+25 -26
View File
@@ -1,9 +1,9 @@
{
"name": "matrix-js-sdk",
"version": "34.0.0",
"version": "34.10.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
},
"scripts": {
"prepack": "yarn build",
@@ -31,13 +31,10 @@
"keywords": [
"matrix-org"
],
"type": "module",
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.ts",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_browser": "./lib/browser-index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"typings": "./lib/index.d.ts",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
@@ -53,14 +50,15 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^6.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^9.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": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.6.0",
"matrix-widget-api": "^1.8.2",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
@@ -82,31 +80,33 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@stylistic/eslint-plugin": "^2.9.0",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/jest": "^29.0.0",
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@types/uuid": "10",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.0.0",
"babel-plugin-search-and-replace": "^1.1.1",
"debug": "^4.3.4",
"eslint": "8.57.0",
"eslint": "8.57.1",
"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": "^28.0.0",
"eslint-plugin-jsdoc": "^48.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "^2.0.1",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "9.11.0",
"fetch-mock": "11.1.5",
"fetch-mock-jest": "^1.5.1",
"husky": "^9.0.0",
"jest": "^29.0.0",
@@ -117,19 +117,18 @@
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.3.2",
"rimraf": "^5.0.0",
"prettier": "3.3.3",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.25.10",
"typedoc": "^0.26.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^2.0.0",
"typescript": "^5.3.3"
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.4.2"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"typings": "./lib/index.d.ts"
}
}
@@ -112,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) {
@@ -120,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 = "";
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env node
const fsProm = require("fs/promises");
const PKGJSON = "package.json";
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env node
const fsProm = require("fs/promises");
const PKGJSON = "package.json";
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
+1
View File
@@ -0,0 +1 @@
switch_package_to_release.cjs
+4 -4
View File
@@ -21,7 +21,7 @@ import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
@@ -169,17 +169,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
const masterKey = await encryptAESSecretStorageItem(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
const selfSigningKey = await encryptAESSecretStorageItem(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
const userSigningKey = await encryptAESSecretStorageItem(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
+238 -17
View File
@@ -19,7 +19,7 @@ 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";
@@ -83,9 +83,12 @@ import {
CrossSigningKey,
CryptoCallbacks,
DecryptionFailureCode,
DeviceIsolationMode,
EventShieldColour,
EventShieldReason,
KeyBackupInfo,
AllDevicesIsolationMode,
OnlySignedDevicesIsolationMode,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { IKeyBackup } from "../../../src/crypto/backup";
@@ -157,7 +160,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 {};
@@ -291,7 +294,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);
}
@@ -746,6 +749,95 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
);
});
describe("IsolationMode decryption tests", () => {
newBackendOnly(
"OnlySigned mode - fails with an error when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new OnlySignedDevicesIsolationMode());
// It will error as an unknown device because we haven't fetched
// the sender's device keys.
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
expect(decryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_SENDER_DEVICE);
},
);
newBackendOnly(
"NoIsolation mode - Decrypts with warning when cross-signed sender is required but sender is not cross-signed",
async () => {
const decryptedEvent = await setUpTestAndDecrypt(new AllDevicesIsolationMode(false));
expect(decryptedEvent.isDecryptionFailure()).toBe(false);
expect(await aliceClient.getCrypto()!.getEncryptionInfoForEvent(decryptedEvent)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
});
},
);
async function setUpTestAndDecrypt(isolationMode: DeviceIsolationMode): Promise<MatrixEvent> {
// This tests that a message will not be decrypted if the sender
// is not sufficiently trusted according to the selected crypto
// mode.
//
// This test is almost the same as the "Alice receives a megolm
// message" test, with the main difference that we set the
// crypto mode.
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
// Start by using Invisible crypto mode
aliceClient.getCrypto()!.setDeviceIsolationMode(isolationMode);
await startClientAndAwaitFirstSync();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [messageEncrypted] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
// it probably won't be decrypted yet, because it takes a while to process the olm keys
return await testUtils.awaitDecryption(event);
}
});
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1419,7 +1511,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
@@ -1484,7 +1576,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()!);
@@ -2180,11 +2272,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;
},
@@ -2333,8 +2425,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
@@ -3103,15 +3271,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
});
describe("Check if the cross signing keys are available for a user", () => {
describe("User identity", () => {
let keyResponder: E2EKeyResponder;
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
const keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
keyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
keyResponder.addKeyReceiver(BOB_TEST_USER_ID, keyReceiver);
keyResponder.addKeyReceiver(TEST_USER_ID, keyReceiver);
keyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
keyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
@@ -3127,6 +3293,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
.getCrypto()!
.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are available for a tracked user", async () => {
@@ -3137,11 +3309,60 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// Alice is the local user and should be tracked !
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
it("Cross signing keys are not available for an unknown user", async () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(false);
});
newBackendOnly("An unverified user changes identity", async () => {
// We have to be tracking Bob's keys, which means we need to share a room with him
syncResponder.sendOrQueueSyncResponse({
...getSyncResponse([BOB_TEST_USER_ID]),
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
// Bob changes his cross-signing keys
keyResponder.addCrossSigningData(testData.BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA);
syncResponder.sendOrQueueSyncResponse({
next_batch: "2",
device_lists: { changed: [BOB_TEST_USER_ID] },
});
await syncPromise(aliceClient);
await aliceClient.getCrypto()!.userHasCrossSigningKeys(BOB_TEST_USER_ID, true);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
expect(verificationStatus.needsUserApproval).toBe(true);
}
// Pinning the new identity should clear the needsUserApproval flag.
await aliceClient.getCrypto()!.pinCurrentUserIdentity(BOB_TEST_USER_ID);
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.needsUserApproval).toBe(false);
}
});
});
@@ -3278,7 +3499,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
return client;
}
function mkEncryptionEvent(content: Object) {
function mkEncryptionEvent(content: object) {
return mkEventCustom({
sender: persistentStoreClient.getSafeUserId(),
type: "m.room.encryption",
@@ -3291,7 +3512,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
*
* @param stateEvents - Additional state events for the test room
*/
function getSyncResponseWithState(stateEvents: Array<Object>) {
function getSyncResponseWithState(stateEvents: Array<object>) {
const roomResponse = {
state: {
events: [
+1 -22
View File
@@ -66,26 +66,6 @@ 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 () => {
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();
// 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 storageKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -470,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();
@@ -0,0 +1,152 @@
/*
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 fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { createClient, MatrixClient } from "../../../src";
import * as testData from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
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();
});
/**
* Integration tests for to-device messages functionality.
*
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("to-device-messages (%s)", (backend: string, initCrypto: InitCrypto) => {
let aliceClient: MatrixClient;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
beforeEach(
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: testData.TEST_USER_ID,
accessToken: "akjgkrgjsalice",
deviceId: testData.TEST_DEVICE_ID,
});
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
new E2EKeyReceiver(homeserverUrl);
const syncResponder = new SyncResponder(homeserverUrl);
// add bob as known user
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
// 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" },
});
fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {});
fetchMock.post(
new URL(
`/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`,
homeserverUrl,
).toString(),
{ filter_id: "fid" },
);
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 () => {
aliceClient.stopClient();
fetchMock.mockReset();
});
describe("encryptToDeviceMessages", () => {
it("returns empty batch for device that is not known", async () => {
await aliceClient.startClient();
const toDeviceBatch = await aliceClient
.getCrypto()
?.encryptToDeviceMessages(
"m.test.event",
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
{
some: "content",
},
);
expect(toDeviceBatch).toBeDefined();
const { batch, eventType } = toDeviceBatch!;
expect(eventType).toBe("m.room.encrypted");
expect(batch.length).toBe(0);
});
it("returns encrypted batch for known device", async () => {
await aliceClient.startClient();
e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA);
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({
one_time_keys: testData.BOB_ONE_TIME_KEYS,
}));
await syncPromise(aliceClient);
const toDeviceBatch = await aliceClient
.getCrypto()
?.encryptToDeviceMessages(
"m.test.event",
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
{
some: "content",
},
);
expect(toDeviceBatch?.batch.length).toBe(1);
expect(toDeviceBatch?.eventType).toBe("m.room.encrypted");
const { deviceId, payload, userId } = toDeviceBatch!.batch[0];
expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID);
expect(userId).toBe(testData.BOB_TEST_USER_ID);
expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2");
expect(payload.sender_key).toEqual(expect.any(String));
expect(payload.ciphertext).toEqual(
expect.objectContaining({
[testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: {
body: expect.any(String),
type: 0,
},
}),
);
// for future: check that bob's device can decrypt the ciphertext?
});
});
});
+4 -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";
@@ -95,6 +95,7 @@ beforeAll(async () => {
// load the rust library. This can take a few seconds on a slow GH worker.
beforeAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-wasm");
await RustSdkCryptoJs.initAsync();
}, 10000);
@@ -1511,7 +1512,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 +1536,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;
+57 -54
View File
@@ -2281,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: KnownMembership.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(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();
});
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", () => {
@@ -267,8 +267,9 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
return -1;
fn();
// TODO: mock timers properly
return -1 as any;
});
jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
@@ -319,8 +320,9 @@ describe("MSC4108SignInWithQR", () => {
it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
return -1;
fn();
// TODO: mock timers properly
return -1 as any;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
+1 -1
View File
@@ -19,7 +19,7 @@ import { logger } from "../src/logger";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
// eslint-disable-next-line @typescript-eslint/no-require-imports
globalThis.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
+6 -4
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";
@@ -28,7 +27,7 @@ export class AccountDataAccumulator {
* Will be updated when fetchMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
* Will be used by `sendSyncResponseWithUpdatedAccountData`
*/
public accountDataEvents: Map<String, any> = new Map();
public accountDataEvents: Map<string, any> = new Map();
/**
* Intercept requests to set a particular type of account data.
@@ -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(
@@ -99,7 +101,7 @@ export class AccountDataAccumulator {
})),
},
});
} catch (err) {
} catch {
// Might fail with "Cannot queue more than one /sync response" if called too often.
// It's ok if it fails here, the sync response is cumulative and will contain
// the latest account data.
+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");
+15 -5
View File
@@ -101,9 +101,8 @@ export const makeGeolocationPosition = ({
}: {
timestamp?: number;
coords: Partial<GeolocationCoordinates>;
}): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
}): GeolocationPosition => {
const { toJSON, ...coordsJSON } = {
accuracy: 1,
latitude: 54.001927,
longitude: -8.253491,
@@ -112,5 +111,16 @@ export const makeGeolocationPosition = ({
heading: null,
speed: null,
...coords,
},
});
};
const posJSON = {
timestamp: timestamp ?? 1647256791840,
coords: {
toJSON: () => coordsJSON,
...coordsJSON,
},
};
return {
toJSON: () => posJSON,
...posJSON,
};
};
@@ -66,6 +66,7 @@ BOB_DATA = {
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
"ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"DoYouSpeakWhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
@@ -208,7 +209,7 @@ def build_test_data(user_data, prefix = "") -> str:
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
return f"""\
result = f"""\
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
@@ -239,7 +240,7 @@ export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
json.dumps(build_cross_signing_keys_data(user_data, user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]), indent=4)
};
/** Signed OTKs, returned by `POST /keys/claim` */
@@ -279,12 +280,20 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
"""
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
if alt_master_key is not None:
result += f"""
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data, alt_master_key), indent=4)
};
"""
def build_cross_signing_keys_data(user_data) -> dict:
return result
def build_cross_signing_keys_data(user_data, master_key_bytes) -> dict:
"""Build the signed cross-signing-keys data for return from /keys/query"""
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(master_key_bytes)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
+47
View File
@@ -449,3 +449,50 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@bob:xyz": {
"keys": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E"
},
"user_id": "@bob:xyz",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
},
"user_id": "@bob:xyz",
"usage": [
"self_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "eDZETBRUw9yW0WJnBZ7vxo12TW09Yb7/47qBPKZzPZzZEvs9M82dnAOtWUv00mcTdp2K9GpeFYDQJ6qLQgxaCA"
}
}
}
},
"user_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
},
"user_id": "@bob:xyz",
"usage": [
"user_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "Q1CbIXvp2BxBsu3F/eZ1ZpuR5rXIt0+FrrA/l6itskpW748xwMoIKxQRVQqs87kh7pCsWEoTy6FzIL8nV+P6BQ"
}
}
}
}
};
+1 -1
View File
@@ -127,7 +127,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
}
} catch (ex) {
} catch {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
+7 -4
View File
@@ -147,10 +147,13 @@ export class MockRTCPeerConnection {
}
constructor() {
this.localDescription = {
const localDescriptionJSON = {
sdp: DUMMY_SDP,
type: "offer",
toJSON: function () {},
type: "offer" as RTCSdpType,
};
this.localDescription = {
toJSON: () => localDescriptionJSON,
...localDescriptionJSON,
};
this.readyToNegotiate = new Promise<void>((resolve) => {
@@ -265,7 +268,7 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
}
export class MockMediaStreamTrack {
+1 -1
View File
@@ -9,7 +9,7 @@ import { defer } from "../../src/utils";
describe("onResumedSync", () => {
let batch: IndexedToDeviceBatch | null;
let shouldFailSendToDevice: Boolean;
let shouldFailSendToDevice: boolean;
let onSendToDeviceFailure: () => void;
let onSendToDeviceSuccess: () => void;
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
@@ -0,0 +1,38 @@
/*
* 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 { keyFromAuthData } from "../../../src/common-crypto/key-passphrase.ts";
describe("key-passphrase", () => {
describe("keyFromAuthData", () => {
it("should throw an error if salt or iterations are missing", async () => {
// missing salt
expect(() => keyFromAuthData({ private_key_iterations: 5 }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);
// missing iterations
expect(() => keyFromAuthData({ private_key_salt: "salt" }, "passphrase")).toThrow(
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
);
});
it("should derive key from auth data", async () => {
const key = await keyFromAuthData({ private_key_salt: "salt", private_key_iterations: 5 }, "passphrase");
expect(key).toBeDefined();
});
});
});
+44
View File
@@ -0,0 +1,44 @@
/*
* 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 { decodeRecoveryKey, encodeRecoveryKey } from "../../../src/crypto-api";
describe("recovery key", () => {
describe("decodeRecoveryKey", () => {
it("should thrown an incorrect length error", () => {
const key = [0, 1];
const encodedKey = encodeRecoveryKey(key)!;
expect(() => decodeRecoveryKey(encodedKey)).toThrow("Incorrect length");
});
it("should thrown an incorrect parity", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
let encodedKey = encodeRecoveryKey(key)!;
// Mutate the encoded key to have incorrect parity
encodedKey = encodedKey.replace("EsSz", "EsSZ");
expect(() => decodeRecoveryKey(encodedKey!)).toThrow("Incorrect parity");
});
it("should decode a valid encoded key", () => {
const key = Array.from({ length: 32 }, (_, i) => i);
const encodedKey = encodeRecoveryKey(key)!;
expect(decodeRecoveryKey(encodedKey)).toEqual(new Uint8Array(key));
});
});
});
+113 -1
View File
@@ -26,6 +26,7 @@ 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";
import type { DeviceInfoMap } from "../../src/crypto/DeviceList";
const Olm = global.Olm;
@@ -607,7 +608,7 @@ describe("Crypto", function () {
event.claimedEd25519Key = null;
try {
await bobClient.crypto!.decryptEvent(event);
} catch (e) {
} catch {
// we expect this to fail because we don't have the
// decryption keys yet
}
@@ -1245,6 +1246,117 @@ describe("Crypto", function () {
});
});
describe("encryptToDeviceMessages", () => {
let client: TestClient;
let ensureOlmSessionsForDevices: jest.SpiedFunction<typeof olmlib.ensureOlmSessionsForDevices>;
let encryptMessageForDevice: jest.SpiedFunction<typeof olmlib.encryptMessageForDevice>;
const payload = { hello: "world" };
let encryptedPayload: object;
let crypto: Crypto;
beforeEach(async () => {
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
result.plaintext = { type: 0, body: JSON.stringify(payload) };
});
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key,
ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } },
};
crypto = client.client.getCrypto() as Crypto;
});
afterEach(async () => {
ensureOlmSessionsForDevices.mockRestore();
encryptMessageForDevice.mockRestore();
await client.stop();
});
it("returns encrypted batch where devices known", async () => {
const deviceInfoMap: DeviceInfoMap = new Map([
[
"@bob:example.org",
new Map([
["bobweb", new DeviceInfo("bobweb")],
["bobmobile", new DeviceInfo("bobmobile")],
]),
],
["@carol:example.org", new Map([["caroldesktop", new DeviceInfo("caroldesktop")]])],
]);
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(deviceInfoMap);
// const deviceInfoMap = await this.downloadKeys(Array.from(userIds), false);
const batch = await client.client.getCrypto()?.encryptToDeviceMessages(
"m.test.type",
[
{ userId: "@bob:example.org", deviceId: "bobweb" },
{ userId: "@bob:example.org", deviceId: "bobmobile" },
{ userId: "@carol:example.org", deviceId: "caroldesktop" },
{ userId: "@carol:example.org", deviceId: "carolmobile" }, // not known
],
payload,
);
expect(crypto.deviceList.downloadKeys).toHaveBeenCalledWith(
["@bob:example.org", "@carol:example.org"],
false,
);
expect(encryptMessageForDevice).toHaveBeenCalledTimes(3);
const expectedPayload = expect.objectContaining({
...encryptedPayload,
"org.matrix.msgid": expect.any(String),
"sender_key": expect.any(String),
});
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch.length).toEqual(3);
expect(batch).toEqual({
eventType: "m.room.encrypted",
batch: expect.arrayContaining([
{
userId: "@bob:example.org",
deviceId: "bobweb",
payload: expectedPayload,
},
{
userId: "@bob:example.org",
deviceId: "bobmobile",
payload: expectedPayload,
},
{
userId: "@carol:example.org",
deviceId: "caroldesktop",
payload: expectedPayload,
},
]),
});
});
it("returns empty batch if no devices known", async () => {
jest.spyOn(crypto.deviceList, "downloadKeys").mockResolvedValue(new Map());
const batch = await crypto.encryptToDeviceMessages(
"m.test.type",
[
{ deviceId: "AAA", userId: "@user1:domain" },
{ deviceId: "BBB", userId: "@user1:domain" },
{ deviceId: "CCC", userId: "@user2:domain" },
],
payload,
);
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch).toEqual([]);
});
});
describe("checkSecretStoragePrivateKey", () => {
let client: TestClient;
+4 -6
View File
@@ -140,9 +140,7 @@ describe("Cross Signing", function () {
});
}
const error = new MatrixError(errorResponse);
error.httpStatus == 401;
throw error;
throw new MatrixError(errorResponse, 401);
};
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
@@ -827,7 +825,7 @@ describe("Cross Signing", function () {
});
it("should offer to upgrade device verifications to cross-signing", async function () {
let upgradeResolveFunc: Function;
let upgradeResolveFunc: () => void;
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@@ -866,7 +864,7 @@ describe("Cross Signing", function () {
// cross-signing key is signed by his Dynabook, which alice has
// verified, and ask if the device verification should be upgraded to a
// cross-signing verification
let upgradePromise = new Promise((resolve) => {
let upgradePromise = new Promise<void>((resolve) => {
upgradeResolveFunc = resolve;
});
await resetCrossSigningKeys(alice);
@@ -885,7 +883,7 @@ describe("Cross Signing", function () {
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
expect(bobTrust2.isTofu()).toBeTruthy();
upgradePromise = new Promise((resolve) => {
upgradePromise = new Promise<void>((resolve) => {
upgradeResolveFunc = resolve;
});
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
+2 -2
View File
@@ -20,7 +20,7 @@ import { IObject } from "../../../src/crypto/olmlib";
import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from "../../TestClient";
import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
@@ -612,7 +612,7 @@ describe("Secrets", function () {
type: "m.megolm_backup.v1",
content: {
encrypted: {
key_id: await encryptAES(
key_id: await encryptAESSecretStorageItem(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
secretStorageKeys.key_id,
"m.megolm_backup.v1",
+1 -1
View File
@@ -454,7 +454,7 @@ describe("SAS verification", function () {
let bobSasEvent: ISasEvent | null;
let aliceVerifier: SAS;
let bobPromise: Promise<void>;
let clearTestClientTimeouts: Function;
let clearTestClientTimeouts: () => void;
beforeEach(async function () {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
+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;
}
});
});
+236 -3
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;
@@ -125,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",
@@ -160,6 +189,199 @@ describe("RoomWidgetClient", () => {
});
});
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] });
@@ -211,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");
+498 -14
View File
@@ -57,6 +57,7 @@ import {
Room,
RuleId,
TweakName,
UpdateDelayedEventAction,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
@@ -97,11 +98,11 @@ 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;
thenCall?: Function;
thenCall?: () => void;
};
interface Options extends ICreateRoomOpts {
@@ -298,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>);
@@ -400,7 +403,7 @@ describe("MatrixClient", function () {
async function assertRequestsMade(
responses: {
prefix?: string;
error?: { httpStatus: Number; errcode: string };
error?: { httpStatus: number; errcode: string };
data?: { event_id: string };
}[],
expectRejects = false,
@@ -704,6 +707,446 @@ 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);
});
});
describe("extended profiles", () => {
const unstableMSC4133Prefix = `${ClientPrefix.Unstable}/uk.tcpip.msc4133`;
const userId = "@profile_user:example.org";
beforeEach(() => {
unstableFeatures["uk.tcpip.msc4133"] = true;
});
it("throws when unsupported by server", async () => {
unstableFeatures["uk.tcpip.msc4133"] = false;
const errorMessage = "Server does not support extended profiles";
await expect(client.doesServerSupportExtendedProfiles()).resolves.toEqual(false);
await expect(client.getExtendedProfile(userId)).rejects.toThrow(errorMessage);
await expect(client.getExtendedProfileProperty(userId, "test_key")).rejects.toThrow(errorMessage);
await expect(client.setExtendedProfileProperty("test_key", "foo")).rejects.toThrow(errorMessage);
await expect(client.deleteExtendedProfileProperty("test_key")).rejects.toThrow(errorMessage);
await expect(client.patchExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
await expect(client.setExtendedProfile({ test_key: "foo" })).rejects.toThrow(errorMessage);
});
it("can fetch a extended user profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "GET",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(userId),
data: testProfile,
},
];
await expect(client.getExtendedProfile(userId)).resolves.toEqual(testProfile);
expect(httpLookups).toHaveLength(0);
});
it("can fetch a property from a extended user profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "GET",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(userId) + "/test_key",
data: testProfile,
},
];
await expect(client.getExtendedProfileProperty(userId, "test_key")).resolves.toEqual("foo");
expect(httpLookups).toHaveLength(0);
});
it("can set a property in our extended profile", async () => {
httpLookups = [
{
method: "PUT",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
expectBody: {
test_key: "foo",
},
},
];
await expect(client.setExtendedProfileProperty("test_key", "foo")).resolves.toEqual(undefined);
expect(httpLookups).toHaveLength(0);
});
it("can delete a property in our extended profile", async () => {
httpLookups = [
{
method: "DELETE",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!) + "/test_key",
},
];
await expect(client.deleteExtendedProfileProperty("test_key")).resolves.toEqual(undefined);
expect(httpLookups).toHaveLength(0);
});
it("can patch our extended profile", async () => {
const testProfile = {
test_key: "foo",
};
const patchedProfile = {
existing: "key",
test_key: "foo",
};
httpLookups = [
{
method: "PATCH",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
data: patchedProfile,
expectBody: testProfile,
},
];
await expect(client.patchExtendedProfile(testProfile)).resolves.toEqual(patchedProfile);
});
it("can replace our extended profile", async () => {
const testProfile = {
test_key: "foo",
};
httpLookups = [
{
method: "PUT",
prefix: unstableMSC4133Prefix,
path: "/profile/" + encodeURIComponent(client.credentials.userId!),
data: testProfile,
expectBody: testProfile,
},
];
await expect(client.setExtendedProfile(testProfile)).resolves.toEqual(undefined);
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
@@ -963,7 +1406,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);
});
});
@@ -1072,7 +1515,7 @@ describe("MatrixClient", function () {
});
describe("emitted sync events", function () {
function syncChecker(expectedStates: [string, string | null][], done: Function) {
function syncChecker(expectedStates: [string, string | null][], done: () => void) {
return function syncListener(state: SyncState, old: SyncState | null) {
const expected = expectedStates.shift();
logger.log("'sync' curr=%s old=%s EXPECT=%s", state, old, expected);
@@ -1094,7 +1537,7 @@ describe("MatrixClient", function () {
it("should transition null -> PREPARED after the first /sync", async () => {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1111,7 +1554,7 @@ describe("MatrixClient", function () {
error: { errcode: "NOPE_NOPE_NOPE" },
});
expectedStates.push(["ERROR", null]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1151,7 +1594,7 @@ describe("MatrixClient", function () {
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1162,7 +1605,7 @@ describe("MatrixClient", function () {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1187,7 +1630,7 @@ describe("MatrixClient", function () {
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1206,7 +1649,7 @@ describe("MatrixClient", function () {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1221,7 +1664,7 @@ describe("MatrixClient", function () {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -1252,7 +1695,7 @@ describe("MatrixClient", function () {
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
const didSyncPromise = new Promise((resolve) => {
const didSyncPromise = new Promise<void>((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
@@ -3035,4 +3478,45 @@ describe("MatrixClient", function () {
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" });
});
});
});
+9 -22
View File
@@ -98,20 +98,6 @@ describe("CallMembership", () => {
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" };
@@ -198,13 +184,6 @@ describe("CallMembership", () => {
beforeEach(() => {
// server origin timestamp for this event is 1000
fakeEvent = makeMockEvent(1000);
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
// (ie. the local clock is 1 second ahead of the servers' clocks)
fakeEvent.localTimestamp = 2000;
// for simplicity's sake, we say that the event's age is zero
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
membership = new CallMembership(fakeEvent!, membershipTemplate);
jest.useFakeTimers();
@@ -215,6 +194,13 @@ describe("CallMembership", () => {
});
it("converts expiry time into local clock", () => {
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
// (ie. the local clock is 1 second ahead of the servers' clocks)
fakeEvent.localTimestamp = 2000;
// for simplicity's sake, we say that the event's age is zero
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
// for sanity's sake, make sure the server-relative expiry time is what we expect
expect(membership.getAbsoluteExpiry()).toEqual(6000);
// therefore the expiry time converted to our clock should be 1 second later
@@ -223,7 +209,8 @@ describe("CallMembership", () => {
it("calculates time until expiry", () => {
jest.setSystemTime(2000);
expect(membership.getMsUntilExpiry()).toEqual(5000);
// should be using absolute expiry time
expect(membership.getMsUntilExpiry()).toEqual(4000);
});
});
});
File diff suppressed because it is too large Load Diff
@@ -102,6 +102,7 @@ describe("MatrixRTCSessionManager", () => {
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"),
isDecryptionFailure: jest.fn().mockReturnValue(false),
sender: {
userId: "@mock:user.example",
},
@@ -110,4 +111,93 @@ describe("MatrixRTCSessionManager", () => {
await new Promise(process.nextTick);
expect(onCallEncryptionMock).toHaveBeenCalled();
});
describe("event decryption", () => {
it("Retries decryption and processes success", async () => {
try {
jest.useFakeTimers();
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
const onCallEncryptionMock = jest.fn();
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
let isDecryptionFailure = true;
client.decryptEventIfNeeded = jest
.fn()
.mockReturnValueOnce(Promise.resolve())
.mockImplementation(() => {
isDecryptionFailure = false;
return Promise.resolve();
});
const timelineEvent = {
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"),
isDecryptionFailure: jest.fn().mockImplementation(() => isDecryptionFailure),
getId: jest.fn().mockReturnValue("event_id"),
sender: {
userId: "@mock:user.example",
},
} as unknown as MatrixEvent;
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// should retry after one second:
await jest.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(1);
} finally {
jest.useRealTimers();
}
});
it("Retries decryption and processes failure", async () => {
try {
jest.useFakeTimers();
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
const onCallEncryptionMock = jest.fn();
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
client.decryptEventIfNeeded = jest.fn().mockReturnValue(Promise.resolve());
const timelineEvent = {
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"),
isDecryptionFailure: jest.fn().mockReturnValue(true), // always fail
getId: jest.fn().mockReturnValue("event_id"),
sender: {
userId: "@mock:user.example",
},
} as unknown as MatrixEvent;
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// should retry after one second:
await jest.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// doesn't retry again:
await jest.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
} finally {
jest.useRealTimers();
}
});
});
});
+7 -7
View File
@@ -20,10 +20,10 @@ import { randomString } from "../../../src/randomstring";
type MembershipData = CallMembershipData[] | SessionMembershipData;
export function makeMockRoom(membershipData: MembershipData, localAge: number | null = null): Room {
export function makeMockRoom(membershipData: MembershipData): Room {
const roomId = randomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(membershipData, roomId, localAge);
const roomState = makeMockRoomState(membershipData, roomId);
return {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
@@ -34,8 +34,8 @@ export function makeMockRoom(membershipData: MembershipData, localAge: number |
} as unknown as Room;
}
export function makeMockRoomState(membershipData: MembershipData, roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(membershipData, roomId, localAge);
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
const event = mockRTCEvent(membershipData, roomId);
return {
on: jest.fn(),
off: jest.fn(),
@@ -57,7 +57,7 @@ export function makeMockRoomState(membershipData: MembershipData, roomId: string
};
}
export function mockRTCEvent(membershipData: MembershipData, roomId: string, localAge: number | null): MatrixEvent {
export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue(
@@ -68,11 +68,11 @@ export function mockRTCEvent(membershipData: MembershipData, roomId: string, loc
},
),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
localTimestamp: Date.now() - (localAge ?? 10),
getTs: jest.fn().mockReturnValue(Date.now()),
getRoomId: jest.fn().mockReturnValue(roomId),
sender: {
userId: "@mock:user.example",
},
isDecryptionFailure: jest.fn().mockReturnValue(false),
} as unknown as MatrixEvent;
}
+4 -55
View File
@@ -107,7 +107,7 @@ describe("MSC3089TreeSpace", () => {
return Promise.resolve();
});
client.invite = fn;
await tree.invite(target, false, false);
await tree.invite(target, false);
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -120,7 +120,7 @@ describe("MSC3089TreeSpace", () => {
return Promise.resolve();
});
client.invite = fn;
await tree.invite(target, false, false);
await tree.invite(target, false);
expect(fn).toHaveBeenCalledTimes(2);
});
@@ -133,7 +133,7 @@ describe("MSC3089TreeSpace", () => {
});
client.invite = fn;
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
await expect(tree.invite(target, false)).rejects.toThrow("MatrixError: Sample Failure");
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -155,61 +155,10 @@ describe("MSC3089TreeSpace", () => {
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
];
await tree.invite(target, true, false);
await tree.invite(target, true);
expect(fn).toHaveBeenCalledTimes(4);
});
it("should share keys with invitees", async () => {
const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId);
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
// Mock the history check as best as possible
const historyVis = "shared";
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
// We're not expecting a super rigid test: the function that calls this internally isn't
// really being tested here.
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
expect(stateKey).toEqual("");
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
});
room.currentState.getStateEvents = historyFn;
// Note: inverse test is implicit from other tests, which disable the call stack of this
// test in order to pass.
await tree.invite(target, false, true);
expect(sendKeysFn).toHaveBeenCalledTimes(1);
expect(historyFn).toHaveBeenCalledTimes(1);
});
it("should not share keys with invitees if inappropriate history visibility", async () => {
const target = targetUser;
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
expect(inviteRoomId).toEqual(roomId);
expect(userIds).toMatchObject([target]);
return Promise.resolve();
});
client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn;
const historyVis = "joined"; // NOTE: Changed.
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
expect(stateKey).toEqual("");
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
});
room.currentState.getStateEvents = historyFn;
await tree.invite(target, false, true);
expect(sendKeysFn).toHaveBeenCalledTimes(0);
expect(historyFn).toHaveBeenCalledTimes(1);
});
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
makePowerLevels(pls);
const fn = jest
+6 -1
View File
@@ -412,7 +412,12 @@ describe("MatrixEvent", () => {
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);
+17 -16
View File
@@ -26,7 +26,6 @@ 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,
@@ -39,11 +38,6 @@ import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-uti
jest.mock("jwt-decode");
const webCrypto = new Crypto();
// save for resetting mocks
const realSubtleCrypto = crypto.subtleCrypto;
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
@@ -62,7 +56,11 @@ describe("oidc authorization", () => {
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
global.TextEncoder = TextEncoder;
});
beforeEach(() => {
const webCrypto = new Crypto();
Object.defineProperty(window, "crypto", {
value: {
getRandomValues,
@@ -70,12 +68,6 @@ describe("oidc authorization", () => {
subtle: webCrypto.subtle,
},
});
global.TextEncoder = TextEncoder;
});
afterEach(() => {
// @ts-ignore reset any ugly mocking we did
crypto.subtleCrypto = realSubtleCrypto;
});
it("should generate authorization params", () => {
@@ -97,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),
);
@@ -113,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);
-171
View File
@@ -1,171 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../olm-loader";
import { LegacyRendezvousFailureReason as RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
import { decodeBase64 } from "../../../src/base64";
import { DummyTransport } from "./DummyTransport";
function makeTransport(name: string) {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
}
describe("ECDHv2", function () {
beforeAll(async function () {
await global.Olm.init();
});
describe("with crypto", () => {
it("initiator wants to sign in", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
const message = { key: "xxx" };
await alice.send(message);
const bobReceive = await bob.receive();
expect(bobReceive).toEqual(message);
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("initiator wants to reciprocate", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
const message = { key: "xxx" };
await bob.send(message);
const aliceReceive = await alice.receive();
expect(aliceReceive).toEqual(message);
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("double connect", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
await expect(alice.connect()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("closed", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
alice.close();
await expect(alice.connect()).rejects.toThrow();
await expect(alice.send({})).rejects.toThrow();
await expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});
it("require ciphertext", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
expect(aliceChecksum).toEqual(bobChecksum);
// send a message without encryption
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
await expect(bob.receive()).rejects.toThrow();
});
it("ciphertext before set up", async function () {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob");
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
await alice.cancel(RendezvousFailureReason.Unknown);
await expect(alice.receive()).rejects.toThrow();
});
});
});
-712
View File
@@ -1,712 +0,0 @@
/*
Copyright 2022 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 MockHttpBackend from "matrix-mock-request";
import "../../olm-loader";
import {
MSC3906Rendezvous,
RendezvousCode,
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousIntent,
} from "../../../src/rendezvous";
import {
ECDHv2RendezvousCode as ECDHRendezvousCode,
MSC3903ECDHPayload,
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
} from "../../../src/rendezvous/channels";
import { Device, MatrixClient } from "../../../src";
import {
MSC3886SimpleHttpRendezvousTransport,
MSC3886SimpleHttpRendezvousTransportDetails,
} from "../../../src/rendezvous/transports";
import { DummyTransport } from "./DummyTransport";
import { decodeBase64 } from "../../../src/base64";
import { logger } from "../../../src/logger";
import { CrossSigningKey, OwnDeviceKeys } from "../../../src/crypto-api";
type UserID = string;
type DeviceID = string;
type Fingerprint = string;
type SimpleDeviceMap = Record<UserID, Record<DeviceID, Fingerprint>>;
function mockDevice(userId: UserID, deviceId: DeviceID, fingerprint: Fingerprint): Device {
return {
deviceId,
userId,
getFingerprint: () => fingerprint,
} as unknown as Device;
}
function mockDeviceMap(
userId: UserID,
deviceId: DeviceID,
deviceKey?: Fingerprint,
otherDevices: SimpleDeviceMap = {},
): Map<string, Map<string, Device>> {
const deviceMap: Map<string, Map<string, Device>> = new Map();
const myDevices: Map<string, Device> = new Map();
if (deviceKey) {
myDevices.set(deviceId, mockDevice(userId, deviceId, deviceKey));
}
deviceMap.set(userId, myDevices);
for (const u in otherDevices) {
let userDevices = deviceMap.get(u);
if (!userDevices) {
userDevices = new Map();
deviceMap.set(u, userDevices);
}
for (const d in otherDevices[u]) {
userDevices.set(d, mockDevice(u, d, otherDevices[u][d]));
}
}
return deviceMap;
}
function makeMockClient(opts: {
userId: UserID;
deviceId: DeviceID;
deviceKey?: Fingerprint;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
msc3886Enabled: boolean;
devices?: SimpleDeviceMap;
verificationFunction?: (
userId: string,
deviceId: string,
verified: boolean,
blocked: boolean,
known: boolean,
) => void;
crossSigningIds?: Partial<Record<CrossSigningKey, string>>;
}): [MatrixClient, Map<string, Map<string, Device>>] {
const deviceMap = mockDeviceMap(opts.userId, opts.deviceId, opts.deviceKey, opts.devices);
return [
{
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
if (feature === "org.matrix.msc3886") {
return opts.msc3886Enabled;
} else if (feature === "org.matrix.msc3882") {
return opts.getLoginTokenEnabled;
} else {
return false;
}
}),
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.getLoginTokenEnabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
},
getCachedCapabilities() {
return opts.msc3882r0Only
? {}
: {
capabilities: {
"m.get_login_token": {
enabled: opts.getLoginTokenEnabled,
},
},
};
},
getUserId() {
return opts.userId;
},
getSafeUserId() {
return opts.userId;
},
getDeviceId() {
return opts.deviceId;
},
baseUrl: "https://example.com",
getCrypto() {
return {
getUserDeviceInfo(
[userId]: string[],
downloadUncached?: boolean,
): Promise<Map<string, Map<string, Device>>> {
return Promise.resolve(deviceMap);
},
getCrossSigningKeyId(key: CrossSigningKey): string | null {
return opts.crossSigningIds?.[key] ?? null;
},
setDeviceVerified(userId: string, deviceId: string, verified: boolean): Promise<void> {
return Promise.resolve();
},
crossSignDevice(deviceId: string): Promise<void> {
return Promise.resolve();
},
getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
return Promise.resolve({
ed25519: opts.deviceKey!,
curve25519: "aaaa",
});
},
};
},
} as unknown as MatrixClient,
deviceMap,
];
}
function makeTransport(name: string, uri = "https://test.rz/123456") {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "http.v1", uri });
}
describe("Rendezvous", function () {
beforeAll(async function () {
await global.Olm.init();
});
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetch;
let transports: DummyTransport<any, MSC3903ECDHPayload>[];
const userId: UserID = "@user:example.com";
beforeEach(function () {
httpBackend = new MockHttpBackend();
fetchFn = httpBackend.fetchFn as typeof global.fetch;
transports = [];
});
afterEach(function () {
transports.forEach((x) => x.cleanup());
});
it("generate and cancel", async function () {
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
msc3886Enabled: false,
getLoginTokenEnabled: true,
msc3882r0Only: true,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
const aliceTransport = new MSC3886SimpleHttpRendezvousTransport({
client: alice,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
expect(aliceRz.code).toBeUndefined();
const codePromise = aliceRz.generateCode();
await httpBackend.flush("");
await aliceRz.generateCode();
expect(typeof aliceRz.code).toBe("string");
await codePromise;
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
"https://fallbackserver/rz/123",
);
httpBackend.when("DELETE", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 204,
headers: {},
},
};
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
await httpBackend.flush("");
await expect(cancelPromise).resolves.toBeUndefined();
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequests();
await aliceRz.close();
});
async function testNoProtocols({
getLoginTokenEnabled,
msc3882r0Only,
}: {
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
}) {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
msc3886Enabled: false,
getLoginTokenEnabled,
msc3882r0Only,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.finish",
outcome: "unsupported",
});
})();
await aliceStartProm;
await bobStartPromise;
}
it("no protocols - r0", async function () {
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true });
});
it("no protocols - stable", async function () {
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false });
});
it("new device declines protocol with outcome unsupported", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.finish", outcome: "unsupported" });
})();
await aliceStartProm;
await bobStartPromise;
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
});
it("new device requests an invalid protocol", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "bad protocol" });
})();
await aliceStartProm;
await bobStartPromise;
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
});
it("decline on existing device", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId: "alice",
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
})();
await aliceStartProm;
await bobStartPromise;
await aliceRz.declineLoginOnExistingDevice();
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: "m.login.finish", outcome: "declined" });
});
it("approve on existing device + no verification", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const [alice] = makeMockClient({
userId: "alice",
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
})();
await aliceStartProm;
await bobStartPromise;
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
const bobCompleteProm = (async () => {
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
await bobEcdh.send({ type: "m.login.finish", outcome: "success" });
})();
await confirmProm;
await bobCompleteProm;
});
async function completeLogin(devices: SimpleDeviceMap) {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;
// alice is already signs in and generates a code
const aliceOnFailure = jest.fn();
const aliceVerification = jest.fn();
const [alice, deviceMap] = makeMockClient({
userId,
deviceId: "ALICE",
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
devices,
deviceKey: "aaaa",
verificationFunction: aliceVerification,
crossSigningIds: {
master: "mmmmm",
},
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
const aliceStartProm = aliceRz.startAfterShowingCode();
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
);
const bobStartPromise = (async () => {
const bobChecksum = await bobEcdh.connect();
logger.info(`Bob checksums is ${bobChecksum} now sending intent`);
// await bobEcdh.send({ type: 'm.login.progress', intent: RendezvousIntent.LOGIN_ON_NEW_DEVICE });
// wait for protocols
logger.info("Bob waiting for protocols");
const protocols = await bobEcdh.receive();
logger.info(`Bob protocols: ${JSON.stringify(protocols)}`);
expect(protocols).toEqual({
type: "m.login.progress",
protocols: ["org.matrix.msc3906.login_token"],
});
await bobEcdh.send({ type: "m.login.progress", protocol: "org.matrix.msc3906.login_token" });
})();
await aliceStartProm;
await bobStartPromise;
const confirmProm = aliceRz.approveLoginOnExistingDevice("token");
const bobLoginProm = (async () => {
const loginToken = await bobEcdh.receive();
expect(loginToken).toEqual({ type: "m.login.progress", login_token: "token", homeserver: alice.baseUrl });
await bobEcdh.send({ type: "m.login.finish", outcome: "success", device_id: "BOB", device_key: "bbbb" });
})();
expect(await confirmProm).toEqual("BOB");
await bobLoginProm;
return {
aliceTransport,
aliceEcdh,
aliceRz,
bobTransport,
bobEcdh,
deviceMap,
};
}
it("approve on existing device + verification", async function () {
const { bobEcdh, aliceRz } = await completeLogin({
[userId]: {
BOB: "bbbb",
},
});
const verifyProm = aliceRz.verifyNewDeviceOnExistingDevice();
const bobVerifyProm = (async () => {
const verified = await bobEcdh.receive();
expect(verified).toEqual({
type: "m.login.finish",
outcome: "verified",
verifying_device_id: "ALICE",
verifying_device_key: "aaaa",
master_key: "mmmmm",
});
})();
await verifyProm;
await bobVerifyProm;
});
it("device not online within timeout", async function () {
const { aliceRz } = await completeLogin({});
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("device appears online within timeout", async function () {
const devices: SimpleDeviceMap = {};
const { aliceRz, deviceMap } = await completeLogin(devices);
// device appears before the timeout
setTimeout(() => {
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
}, 1000);
await aliceRz.verifyNewDeviceOnExistingDevice(2000);
});
it("device appears online after timeout", async function () {
const devices: SimpleDeviceMap = {};
const { aliceRz, deviceMap } = await completeLogin(devices);
// device appears after the timeout
setTimeout(() => {
deviceMap.get(userId)!.set("BOB", mockDevice(userId, "BOB", "bbbb"));
}, 1500);
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("mismatched device key", async function () {
const { aliceRz } = await completeLogin({
[userId]: {
BOB: "XXXX",
},
});
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
});
});
@@ -1,453 +0,0 @@
/*
Copyright 2022 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 MockHttpBackend from "matrix-mock-request";
import type { MatrixClient } from "../../../src";
import { LegacyRendezvousFailureReason as RendezvousFailureReason } from "../../../src/rendezvous";
import { MSC3886SimpleHttpRendezvousTransport } from "../../../src/rendezvous/transports";
function makeMockClient(opts: { userId: string; deviceId: string; msc3886Enabled: boolean }): MatrixClient {
return {
doesServerSupportUnstableFeature(feature: string) {
return Promise.resolve(opts.msc3886Enabled && feature === "org.matrix.msc3886");
},
getUserId() {
return opts.userId;
},
getDeviceId() {
return opts.deviceId;
},
requestLoginToken() {
return Promise.resolve({ login_token: "token" });
},
baseUrl: "https://example.com",
} as unknown as MatrixClient;
}
describe("SimpleHttpRendezvousTransport", function () {
let httpBackend: MockHttpBackend;
let fetchFn: typeof global.fetch;
beforeEach(function () {
httpBackend = new MockHttpBackend();
fetchFn = httpBackend.fetchFn as typeof global.fetch;
});
async function postAndCheckLocation(
msc3886Enabled: boolean,
fallbackRzServer: string,
locationResponse: string,
expectedFinalLocation: string,
) {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fallbackRzServer, fetchFn });
{
// initial POST
const expectedPostLocation = msc3886Enabled
? `${client.baseUrl}/_matrix/client/unstable/org.matrix.msc3886/rendezvous`
: fallbackRzServer;
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", expectedPostLocation).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: locationResponse,
},
},
};
await httpBackend.flush("");
await prom;
}
const details = await simpleHttpTransport.details();
expect(details.uri).toBe(expectedFinalLocation);
{
// first GET without etag
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", expectedFinalLocation).response = {
body: {},
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
},
},
};
await httpBackend.flush("");
expect(await prom).toEqual({});
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
}
}
it("should throw an error when no server available", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
await expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
});
it("POST to fallback server", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
});
it("POST with no location", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {},
},
};
await Promise.all([expect(prom).rejects.toThrow(), httpBackend.flush("")]);
});
it("POST with absolute path response", async function () {
await postAndCheckLocation(false, "https://fallbackserver/rz", "/123", "https://fallbackserver/123");
});
it("POST to built-in MSC3886 implementation", async function () {
await postAndCheckLocation(
true,
"https://fallbackserver/rz",
"123",
"https://example.com/_matrix/client/unstable/org.matrix.msc3886/rendezvous/123",
);
});
it("POST with relative path response including parent", async function () {
await postAndCheckLocation(
false,
"https://fallbackserver/rz/abc",
"../xyz/123",
"https://fallbackserver/rz/xyz/123",
);
});
it("POST to follow 307 to other server", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const prom = simpleHttpTransport.send({});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 307,
headers: {
location: "https://redirected.fallbackserver/rz",
},
},
};
httpBackend.when("POST", "https://redirected.fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://redirected.fallbackserver/rz/123",
etag: "aaa",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
});
it("POST and GET", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{
// initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
}
{
// first GET without etag
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
"etag": "aaa",
},
},
};
await httpBackend.flush("");
expect(await prom).toEqual({ foo: "baa" });
}
{
// subsequent GET which should have etag from previous request
const prom = simpleHttpTransport.receive();
httpBackend.when("GET", "https://fallbackserver/rz/123").check(({ headers }) => {
expect(headers["if-none-match"]).toEqual("aaa");
}).response = {
body: { foo: "baa" },
response: {
statusCode: 200,
headers: {
"content-type": "application/json",
"etag": "bbb",
},
},
};
await httpBackend.flush("");
expect(await prom).toEqual({ foo: "baa" });
}
});
it("POST and PUTs", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{
// initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("", 1);
await prom;
}
{
// first PUT without etag
const prom = simpleHttpTransport.send({ a: "b" });
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers, data }) => {
expect(headers["if-match"]).toBeUndefined();
expect(data).toEqual({ a: "b" });
}).response = {
body: null,
response: {
statusCode: 202,
headers: {
etag: "aaa",
},
},
};
await httpBackend.flush("", 1);
await prom;
}
{
// subsequent PUT which should have etag from previous request
const prom = simpleHttpTransport.send({ c: "d" });
httpBackend.when("PUT", "https://fallbackserver/rz/123").check(({ headers }) => {
expect(headers["if-match"]).toEqual("aaa");
}).response = {
body: null,
response: {
statusCode: 202,
headers: {
etag: "bbb",
},
},
};
await httpBackend.flush("", 1);
await prom;
}
});
it("POST and DELETE", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
{
// Create
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").check(({ headers, data }) => {
expect(headers["content-type"]).toEqual("application/json");
expect(data).toEqual({ foo: "baa" });
}).response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
},
},
};
await httpBackend.flush("");
expect(await prom).toStrictEqual(undefined);
}
{
// Cancel
const prom = simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
httpBackend.when("DELETE", "https://fallbackserver/rz/123").response = {
body: null,
response: {
statusCode: 204,
headers: {},
},
};
await httpBackend.flush("");
await prom;
}
});
it("details before ready", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
await expect(simpleHttpTransport.details()).rejects.toThrow();
});
it("send after cancelled", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
await expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
});
it("receive before ready", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
await expect(simpleHttpTransport.receive()).rejects.toThrow();
});
it("404 failure callback", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const onFailure = jest.fn();
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
onFailure,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 404,
headers: {},
},
};
await Promise.all([
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(),
httpBackend.flush("", 1),
]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
});
it("404 failure callback mapped to expired", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const onFailure = jest.fn();
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
client,
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
onFailure,
});
{
// initial POST
const prom = simpleHttpTransport.send({ foo: "baa" });
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
statusCode: 201,
headers: {
location: "https://fallbackserver/rz/123",
expires: "Thu, 01 Jan 1970 00:00:00 GMT",
},
},
};
await httpBackend.flush("");
await prom;
}
{
// GET with 404 to simulate expiry
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
statusCode: 404,
headers: {},
},
};
await Promise.all([expect(simpleHttpTransport.receive()).resolves.toBeUndefined(), httpBackend.flush("")]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
}
});
});
+55
View File
@@ -1122,4 +1122,59 @@ describe("RoomState", function () {
).toBeFalsy();
});
});
describe("skipWrongOrderRoomStateInserts", () => {
const idNow = "now";
const idBefore = "before";
const endState = new RoomState(roomId);
const startState = new RoomState(roomId, undefined, true);
let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
const evNow = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
evNow.event.unsigned = { replaces_state: idBefore };
evNow.event.event_id = idNow;
const evBefore = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
const updatedStateWithBefore = jest.fn();
const updatedStateWithNow = jest.fn();
beforeEach(() => {
evBefore.event.event_id = idBefore;
onRoomStateEvent = (event, _state, _lastState) => {
if (event.event.event_id === idNow) {
updatedStateWithNow();
} else if (event.event.event_id === idBefore) {
updatedStateWithBefore();
}
};
endState.on(RoomStateEvent.Events, onRoomStateEvent);
startState.on(RoomStateEvent.Events, onRoomStateEvent);
});
afterEach(() => {
endState.off(RoomStateEvent.Events, onRoomStateEvent);
startState.off(RoomStateEvent.Events, onRoomStateEvent);
updatedStateWithNow.mockReset();
updatedStateWithBefore.mockReset();
});
it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => {
endState.setStateEvents([evNow, evBefore]);
expect(updatedStateWithBefore).not.toHaveBeenCalled();
expect(updatedStateWithNow).toHaveBeenCalled();
});
it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => {
startState.setStateEvents([evBefore, evNow]);
expect(updatedStateWithBefore).toHaveBeenCalled();
expect(updatedStateWithNow).not.toHaveBeenCalled();
});
});
});
@@ -208,7 +208,7 @@ describe("OutgoingRequestProcessor", () => {
const outgoingRequest = new UploadSigningKeysRequest(JSON.stringify(testReq));
// also create a UIA callback
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
return await makeRequest({ type: "test" });
};
@@ -458,7 +458,7 @@ describe("OutgoingRequestProcessor", () => {
throw new Error("Failed to fetch");
}
});
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
const authCallback: UIAuthCallback<object> = async (makeRequest) => {
return await makeRequest({ type: "test" });
};
const requestPromise = processor.makeOutgoingRequest(outgoingRequest, authCallback);
@@ -26,7 +26,6 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
import * as TestData from "../../test-utils/test-data";
import {
ConnectionError,
CryptoEvent,
HttpApiEvent,
HttpApiEventHandlerMap,
IHttpOpts,
@@ -37,6 +36,7 @@ import {
import * as testData from "../../test-utils/test-data";
import { BackupDecryptor } from "../../../src/common-crypto/CryptoBackend";
import { KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
describe("PerSessionKeyBackupDownloader", () => {
/** The downloader under test */
+112 -9
View File
@@ -17,6 +17,7 @@
*/
import {
CollectStrategy,
Curve25519PublicKey,
Ed25519PublicKey,
HistoryVisibility as RustHistoryVisibility,
@@ -31,6 +32,7 @@ import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
import { defer } from "../../../src/utils";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { KnownMembership } from "../../../src/@types/membership";
import { DeviceIsolationMode, AllDevicesIsolationMode, OnlySignedDevicesIsolationMode } from "../../../src/crypto-api";
describe("RoomEncryptor", () => {
describe("History Visibility", () => {
@@ -99,7 +101,7 @@ describe("RoomEncryptor", () => {
getEncryptionTargetMembers: jest.fn().mockReturnValue([mockRoomMember]),
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(true),
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Invited),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(null),
} as unknown as Mocked<Room>;
roomEncryptor = new RoomEncryptor(
@@ -111,6 +113,8 @@ describe("RoomEncryptor", () => {
);
});
const defaultDevicesIsolationMode = new AllDevicesIsolationMode(false);
it("should ensure that there is only one shareRoomKey at a time", async () => {
const deferredShare = defer<void>();
const insideOlmShareRoom = defer<void>();
@@ -120,19 +124,19 @@ describe("RoomEncryptor", () => {
await deferredShare.promise;
});
roomEncryptor.prepareForEncryption(false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
await insideOlmShareRoom.promise;
// call several times more
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
roomEncryptor.prepareForEncryption(false);
roomEncryptor.encryptEvent(createMockEvent("World"), false);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("Hello"), false, defaultDevicesIsolationMode);
roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
roomEncryptor.encryptEvent(createMockEvent("World"), false, defaultDevicesIsolationMode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(1);
deferredShare.resolve();
await roomEncryptor.prepareForEncryption(false);
await roomEncryptor.prepareForEncryption(false, defaultDevicesIsolationMode);
// should have been called again
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalledTimes(6);
@@ -158,8 +162,16 @@ describe("RoomEncryptor", () => {
let firstMessageFinished: string | null = null;
const firstRequest = roomEncryptor.encryptEvent(createMockEvent("Hello"), false);
const secondRequest = roomEncryptor.encryptEvent(createMockEvent("Edit of Hello"), false);
const firstRequest = roomEncryptor.encryptEvent(
createMockEvent("Hello"),
false,
defaultDevicesIsolationMode,
);
const secondRequest = roomEncryptor.encryptEvent(
createMockEvent("Edit of Hello"),
false,
defaultDevicesIsolationMode,
);
firstRequest.then(() => {
if (firstMessageFinished === null) {
@@ -181,5 +193,96 @@ describe("RoomEncryptor", () => {
expect(firstMessageFinished).toBe("hello");
});
describe("DeviceIsolationMode", () => {
type TestCase = [
string,
{
mode: DeviceIsolationMode;
expectedStrategy: CollectStrategy;
globalBlacklistUnverifiedDevices: boolean;
},
];
const testCases: TestCase[] = [
[
"Share AllDevicesIsolationMode",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, false),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - with blacklist unverified",
{
mode: new AllDevicesIsolationMode(false),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, false),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned - blacklist true",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: true,
},
],
[
"Share OnlySigned",
{
mode: new OnlySignedDevicesIsolationMode(),
expectedStrategy: CollectStrategy.identityBasedStrategy(),
globalBlacklistUnverifiedDevices: false,
},
],
[
"Share AllDevicesIsolationMode - Verified user problems true",
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(false, true),
globalBlacklistUnverifiedDevices: false,
},
],
[
'Share AllDevicesIsolationMode - with blacklist unverified - Verified user problems true"',
{
mode: new AllDevicesIsolationMode(true),
expectedStrategy: CollectStrategy.deviceBasedStrategy(true, true),
globalBlacklistUnverifiedDevices: true,
},
],
];
let capturedSettings: CollectStrategy | undefined = undefined;
beforeEach(() => {
capturedSettings = undefined;
mockOlmMachine.shareRoomKey.mockImplementationOnce(async (roomId, users, encryptionSettings) => {
capturedSettings = encryptionSettings.sharingStrategy;
});
});
it.each(testCases)(
"prepareForEncryption should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.prepareForEncryption(globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
it.each(testCases)(
"encryptEvent should properly set sharing strategy based on crypto mode: %s",
async (_, { mode, expectedStrategy, globalBlacklistUnverifiedDevices }) => {
await roomEncryptor.encryptEvent(createMockEvent("Hello"), globalBlacklistUnverifiedDevices, mode);
expect(mockOlmMachine.shareRoomKey).toHaveBeenCalled();
expect(capturedSettings).toBeDefined();
expect(expectedStrategy.eq(capturedSettings!)).toBeTruthy();
},
);
});
});
});
+2 -1
View File
@@ -2,7 +2,8 @@ import { Mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { CryptoEvent, HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import * as testData from "../../test-utils/test-data";
import * as TestData from "../../test-utils/test-data";
+163 -5
View File
@@ -30,7 +30,6 @@ import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import {
CryptoEvent,
Device,
DeviceVerification,
encodeBase64,
@@ -69,8 +68,9 @@ import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { ClientEvent, ClientEventHandlerMap } from "../../../src/client";
import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
import { encryptAES } from "../../../src/crypto/aes";
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
@@ -95,6 +95,7 @@ describe("initRustCrypto", () => {
deleteSecretsFromInbox: jest.fn(),
registerReceiveSecretCallback: jest.fn(),
registerDevicesUpdatedCallback: jest.fn(),
registerRoomKeysWithheldCallback: jest.fn(),
outgoingRequests: jest.fn(),
isBackupEnabled: jest.fn().mockResolvedValue(false),
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
@@ -424,7 +425,7 @@ describe("initRustCrypto", () => {
}, 10000);
async function encryptAndStoreSecretKey(type: string, key: Uint8Array, pickleKey: string, store: CryptoStore) {
const encryptedKey = await encryptAES(encodeBase64(key), Buffer.from(pickleKey), type);
const encryptedKey = await encryptAESSecretStorageItem(encodeBase64(key), Buffer.from(pickleKey), type);
store.storeSecretStorePrivateKey(undefined, type as keyof SecretStorePrivateKeys, encryptedKey);
}
@@ -1361,13 +1362,52 @@ describe("RustCrypto", () => {
});
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
olmMachine.getIdentity.mockResolvedValue({ free: jest.fn(), isVerified: jest.fn().mockReturnValue(true) });
olmMachine.getIdentity.mockResolvedValue({
free: jest.fn(),
isVerified: jest.fn().mockReturnValue(true),
wasPreviouslyVerified: jest.fn().mockReturnValue(true),
});
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.isVerified()).toBeTruthy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeTruthy();
});
});
describe("pinCurrentIdentity", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
it("throws an error for an unknown user", async () => {
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of unknown user",
);
});
it("throws an error for our own user", async () => {
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
"Cannot pin identity of own user",
);
});
});
@@ -1553,6 +1593,124 @@ describe("RustCrypto", () => {
await expect(rustCrypto.exportSecretsBundle()).resolves.toEqual(expect.objectContaining(bundle));
});
});
describe("encryptToDeviceMessages", () => {
let rustCrypto: RustCrypto;
let testOlmMachine: RustSdkCryptoJs.OlmMachine;
beforeEach(async () => {
testOlmMachine = await OlmMachine.initialize(
new RustSdkCryptoJs.UserId(testData.TEST_USER_ID),
new RustSdkCryptoJs.DeviceId(testData.TEST_DEVICE_ID),
);
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
rustCrypto = await makeTestRustCrypto();
expect(OlmMachine.initFromStore).toHaveBeenCalled();
});
afterEach(() => {
testOlmMachine?.free();
});
const payload = { hello: "world" };
it("returns empty batch if devices not known", async () => {
const getMissingSessions = jest.spyOn(testOlmMachine, "getMissingSessions");
const getDevice = jest.spyOn(testOlmMachine, "getDevice");
const batch = await rustCrypto.encryptToDeviceMessages(
"m.test.type",
[
{ deviceId: "AAA", userId: "@user1:domain" },
{ deviceId: "BBB", userId: "@user1:domain" },
{ deviceId: "CCC", userId: "@user2:domain" },
],
payload,
);
expect(getMissingSessions.mock.calls[0][0].length).toBe(2);
expect(getDevice).toHaveBeenCalledTimes(3);
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch).toEqual([]);
});
it("returns encrypted batch for known devices", async () => {
// Make m aware of another device, and get some OTK to be able to establish a session.
await testOlmMachine.markRequestAsSent(
"foo",
RustSdkCryptoJs.RequestType.KeysQuery,
JSON.stringify({
device_keys: {
"@example:localhost": {
AFGUOBTZWM: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "AFGUOBTZWM",
keys: {
"curve25519:AFGUOBTZWM": "boYjDpaC+7NkECQEeMh5dC+I1+AfriX0VXG2UV7EUQo",
"ed25519:AFGUOBTZWM": "NayrMQ33ObqMRqz6R9GosmHdT6HQ6b/RX/3QlZ2yiec",
},
signatures: {
"@example:localhost": {
"ed25519:AFGUOBTZWM":
"RoSWvru1jj6fs2arnTedWsyIyBmKHMdOu7r9gDi0BZ61h9SbCK2zLXzuJ9ZFLao2VvA0yEd7CASCmDHDLYpXCA",
},
},
user_id: "@example:localhost",
unsigned: {
device_display_name: "rust-sdk",
},
},
},
},
failures: {},
}),
);
await testOlmMachine.markRequestAsSent(
"bar",
RustSdkCryptoJs.RequestType.KeysClaim,
JSON.stringify({
one_time_keys: {
"@example:localhost": {
AFGUOBTZWM: {
"signed_curve25519:AAAABQ": {
key: "9IGouMnkB6c6HOd4xUsNv4i3Dulb4IS96TzDordzOws",
signatures: {
"@example:localhost": {
"ed25519:AFGUOBTZWM":
"2bvUbbmJegrV0eVP/vcJKuIWC3kud+V8+C0dZtg4dVovOSJdTP/iF36tQn2bh5+rb9xLlSeztXBdhy4c+LiOAg",
},
},
},
},
},
},
failures: {},
}),
);
const batch = await rustCrypto.encryptToDeviceMessages(
"m.test.type",
[
{ deviceId: "AAA", userId: "@user1:domain" },
{ deviceId: "BBB", userId: "@user1:domain" },
{ deviceId: "CCC", userId: "@user2:domain" },
{ deviceId: "AFGUOBTZWM", userId: "@example:localhost" },
],
payload,
);
expect(batch?.eventType).toEqual("m.room.encrypted");
expect(batch?.batch.length).toEqual(1);
expect(batch?.batch[0].deviceId).toEqual("AFGUOBTZWM");
expect(batch?.batch[0].userId).toEqual("@example:localhost");
expect(batch?.batch[0].payload).toEqual(
expect.objectContaining({
"algorithm": "m.olm.v1.curve25519-aes-sha2",
"ciphertext": expect.any(Object),
"org.matrix.msgid": expect.any(String),
"sender_key": expect.any(String),
}),
);
});
});
});
/** Build a MatrixHttpApi instance */
+1 -1
View File
@@ -98,7 +98,7 @@ describe("secret-storage", () => {
} else if (secretName == "secretE") {
return { aaaa: {}, bbbb: {} };
} else {
null;
return null;
}
}),
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
+1 -1
View File
@@ -12,7 +12,7 @@ jest.useFakeTimers();
describe("MatrixScheduler", function () {
let scheduler: MatrixScheduler<Record<string, boolean>>;
let retryFn: Function | null;
let retryFn: ((event: MatrixEvent | null, attempt: number, err: MatrixError) => number) | null;
let queueFn: ((event: MatrixEvent) => string | null) | null;
let deferred: IDeferred<Record<string, boolean>>;
const roomId = "!foo:bar";
+1 -1
View File
@@ -18,6 +18,7 @@ import { Mocked } from "jest-mock";
import {
AccountDataClient,
calculateKeyCheck,
PassphraseInfo,
SecretStorageCallbacks,
SecretStorageKeyDescriptionAesV1,
@@ -25,7 +26,6 @@ import {
ServerSideSecretStorageImpl,
trimTrailingEquals,
} from "../../src/secret-storage";
import { calculateKeyCheck } from "../../src/crypto/aes";
import { randomString } from "../../src/randomstring";
describe("ServerSideSecretStorageImpl", function () {
+1 -1
View File
@@ -434,7 +434,7 @@ describe("TimelineWindow", function () {
});
function idsOf(events: Array<MatrixEvent>): Array<string> {
return events.map((e) => (e ? e.getId() ?? "MISSING_ID" : "MISSING_EVENT"));
return events.map((e) => (e ? (e.getId() ?? "MISSING_ID") : "MISSING_EVENT"));
}
describe("removing events", () => {
+1 -1
View File
@@ -1228,7 +1228,7 @@ describe("Call", function () {
});
describe("Screen sharing", () => {
const waitNegotiateFunc = (resolve: Function): void => {
const waitNegotiateFunc = (resolve: () => void): void => {
mockSendEvent.mockImplementationOnce(() => {
// Note that the peer connection here is a dummy one and always returns
// dummy SDP, so there's not much point returning the content: the SDP will
+1 -1
View File
@@ -101,7 +101,7 @@ describe("CallFeed", () => {
it.each([
[CallState.Connected, true],
[CallState.Connecting, false],
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
])("should react to call state, when !isLocal()", (state: CallState, expected: boolean) => {
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());
expect(feed.connected).toBe(expected);
+1 -1
View File
@@ -105,7 +105,7 @@ const mockGetStateEvents =
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
if (type === EventType.GroupCallMemberPrefix) {
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
return userId === undefined ? events : (events.find((e) => e.getStateKey() === userId) ?? null);
} else {
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
return userId === undefined ? [fakeEvent] : fakeEvent;
@@ -0,0 +1,29 @@
/*
* 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.
*/
/**
* An AES-encrypted secret storage payload.
* See https://spec.matrix.org/v1.11/client-server-api/#msecret_storagev1aes-hmac-sha2-1
*/
export interface AESEncryptedSecretStoragePayload {
[key: string]: any; // extensible
/** the initialization vector in base64 */
iv: string;
/** the ciphertext in base64 */
ciphertext: string;
/** the HMAC in base64 */
mac: string;
}
+2 -2
View File
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
import { IClientWellKnown } from "../client";
import { UnstableValue } from "../NamespacedValue.ts";
import { IClientWellKnown } from "../client.ts";
// disable lint because these are wire responses
/* eslint-disable camelcase */
+3 -3
View File
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { RelatesToRelationship, REFERENCE_RELATION } from "./extensible_events";
import { UnstableValue } from "../NamespacedValue";
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
import { RelatesToRelationship, REFERENCE_RELATION } from "./extensible_events.ts";
import { UnstableValue } from "../NamespacedValue.ts";
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location.ts";
/**
* Beacon info and beacon event types as described in MSC3672
+2 -2
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { ISignatures } from "./signed";
import type { ISignatures } from "./signed.ts";
export type OlmGroupSessionExtraData = {
untrusted?: boolean;
@@ -22,7 +22,7 @@ export type OlmGroupSessionExtraData = {
};
// Backwards compatible re-export
export type { EventDecryptionResult as IEventDecryptionResult } from "../common-crypto/CryptoBackend";
export type { EventDecryptionResult as IEventDecryptionResult } from "../common-crypto/CryptoBackend.ts";
interface Extensible {
[key: string]: any;
+11 -11
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { NamespacedValue, UnstableValue } from "../NamespacedValue";
import { NamespacedValue, UnstableValue } from "../NamespacedValue.ts";
import {
PolicyRuleEventContent,
RoomAvatarEventContent,
@@ -34,16 +34,16 @@ import {
RoomTopicEventContent,
SpaceChildEventContent,
SpaceParentEventContent,
} from "./state_events";
} from "./state_events.ts";
import {
ExperimentalGroupCallRoomMemberState,
IGroupCallRoomMemberState,
IGroupCallRoomState,
} from "../webrtc/groupCall";
import { MSC3089EventContent } from "../models/MSC3089Branch";
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon";
import { XOR } from "./common";
import { ReactionEventContent, RoomMessageEventContent, StickerEventContent } from "./events";
} from "../webrtc/groupCall.ts";
import { MSC3089EventContent } from "../models/MSC3089Branch.ts";
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon.ts";
import { XOR } from "./common.ts";
import { ReactionEventContent, RoomMessageEventContent, StickerEventContent } from "./events.ts";
import {
MCallAnswer,
MCallBase,
@@ -54,10 +54,10 @@ import {
MCallSelectAnswer,
SDPStreamMetadata,
SDPStreamMetadataKey,
} from "../webrtc/callEventTypes";
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
import { SessionMembershipData } from "../matrixrtc/CallMembership";
} from "../webrtc/callEventTypes.ts";
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts";
import { SessionMembershipData } from "../matrixrtc/CallMembership.ts";
export enum EventType {
// Room state events
+3 -3
View File
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MsgType, RelationType } from "./event";
import { FileInfo, ImageInfo, MediaEventContent } from "./media";
import { XOR } from "./common";
import { MsgType, RelationType } from "./event.ts";
import { FileInfo, ImageInfo, MediaEventContent } from "./media.ts";
import { XOR } from "./common.ts";
interface BaseTimelineEvent {
"body": string;
+1 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { EitherAnd, NamespacedValue, Optional, UnstableValue } from "matrix-events-sdk";
import { isProvided } from "../extensible_events_v1/utilities";
import { isProvided } from "../extensible_events_v1/utilities.ts";
// Types and utilities for MSC1767: Extensible events (version 1) in Matrix
-45
View File
@@ -29,20 +29,11 @@ declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
// marker variable used to detect both the browser & node entrypoints being used at once
__js_sdk_entrypoint: unknown;
}
}
interface Window {
webkitAudioContext: typeof AudioContext;
}
interface Crypto {
webkitSubtle?: Window["crypto"]["subtle"];
}
interface MediaDevices {
// This is experimental and types don't know about it yet
// https://github.com/microsoft/TypeScript/issues/33232
@@ -76,40 +67,4 @@ declare global {
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
}
export interface ISettledFulfilled<T> {
status: "fulfilled";
value: T;
}
export interface ISettledRejected {
status: "rejected";
reason: any;
}
interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
}
interface RTCRtpTransceiver {
// This has been removed from TS
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
// but we still need this for MatrixCall::getRidOfRTXCodecs()
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
}
interface RequestInit {
/**
* Specifies the priority of the fetch request relative to other requests of the same type.
* Must be one of the following strings:
* high: A high priority fetch request relative to other requests of the same type.
* low: A low priority fetch request relative to other requests of the same type.
* auto: Automatically determine the priority of the fetch request relative to other requests of the same type (default).
*
* @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attribute
* @see https://github.com/microsoft/TypeScript/issues/54472
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility
* Not yet supported in Safari or Firefox
*/
priority?: "high" | "low" | "auto";
}
}
+2 -2
View File
@@ -17,8 +17,8 @@ limitations under the License.
// Types for MSC3488 - m.location: Extending events with location data
import { EitherAnd } from "matrix-events-sdk";
import { UnstableValue } from "../NamespacedValue";
import { M_TEXT } from "./extensible_events";
import { UnstableValue } from "../NamespacedValue.ts";
import { M_TEXT } from "./extensible_events.ts";
export enum LocationAssetType {
Self = "m.self",
+30 -37
View File
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MsgType } from "../@types/event";
import { MsgType } from "../@types/event.ts";
/**
* Information on encrypted media attachments.
*
* Used within `m.room.message` events that reference files, such as `m.file` and `m.image`.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#extensions-to-mroommessage-msgtypes
* @see https://spec.matrix.org/v1.11/client-server-api/#extensions-to-mroommessage-msgtypes
*/
export interface EncryptedFile {
/**
@@ -84,13 +84,13 @@ interface BaseInfo {
*
* Used within `m.room.message` events that reference files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
*/
export interface FileInfo extends BaseInfo {
/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
* Only present if the thumbnail is encrypted.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
*/
thumbnail_file?: EncryptedFile;
/**
@@ -108,7 +108,7 @@ export interface FileInfo extends BaseInfo {
*
* Used within `m.room.message` events that reference images.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
*/
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
@@ -117,7 +117,7 @@ export interface ImageInfo extends FileInfo, ThumbnailInfo {}
*
* Used within `m.room.message` events that reference audio files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
*/
export interface AudioInfo extends BaseInfo {
/**
@@ -131,7 +131,7 @@ export interface AudioInfo extends BaseInfo {
*
* Used within `m.room.message` events that reference video files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
*/
export interface VideoInfo extends AudioInfo, ImageInfo {
/**
@@ -148,30 +148,39 @@ export type MediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
interface BaseContent {
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
* @see https://spec.matrix.org/v1.11/client-server-api/#sending-encrypted-attachments
*/
file?: EncryptedFile;
/**
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
*/
url?: string;
}
/**
* Content format of media events with msgtype `m.file`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* A human-readable description of the file.
* This is recommended to be the filename of the original upload.
* If filename is not set or the value of both properties are identical,
* this is the filename of the original upload. Otherwise, this is a
* caption for the file.
*/
body: string;
/**
* The original filename of the uploaded file.
*/
filename?: string;
/**
* The format used in the `formatted_body`.
*/
format?: "org.matrix.custom.html";
/**
* The formatted version of the `body`, when it acts as a caption. This is required if `format` is specified.
*/
formatted_body?: string;
}
/**
* Content format of media events with msgtype `m.file`
*
* @see https://spec.matrix.org/v1.11/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* Information about the file referred to in url.
*/
@@ -185,15 +194,9 @@ export interface FileContent extends BaseContent {
/**
* Content format of media events with msgtype `m.image`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
* @see https://spec.matrix.org/v1.11/client-server-api/#mimage
*/
export interface ImageContent extends BaseContent {
/**
* A textual representation of the image.
* This could be the alt text of the image, the filename of the image,
* or some kind of content description for accessibility e.g. image attachment.
*/
body: string;
/**
* Metadata about the image referred to in url.
*/
@@ -207,14 +210,9 @@ export interface ImageContent extends BaseContent {
/**
* Content format of media events with msgtype `m.audio`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
* @see https://spec.matrix.org/v1.11/client-server-api/#maudio
*/
export interface AudioContent extends BaseContent {
/**
* A description of the audio e.g. Bee Gees - Stayin Alive,
* or some kind of content description for accessibility e.g. audio attachment.
*/
body: string;
/**
* Metadata for the audio clip referred to in url.
*/
@@ -228,14 +226,9 @@ export interface AudioContent extends BaseContent {
/**
* Content format of media events with msgtype `m.video`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
* @see https://spec.matrix.org/v1.11/client-server-api/#mvideo
*/
export interface VideoContent extends BaseContent {
/**
* A description of the video e.g. Gangnam style,
* or some kind of content description for accessibility e.g. video attachment.
*/
body: string;
/**
* Metadata about the video clip referred to in url.
*/
+1 -1
View File
@@ -21,7 +21,7 @@ import {
REFERENCE_RELATION,
RelatesToRelationship,
TSNamespace,
} from "./extensible_events";
} from "./extensible_events.ts";
/**
* Identifier for a disclosed poll.
+1 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { AuthDict } from "../interactive-auth";
import { AuthDict } from "../interactive-auth.ts";
/**
* The request body of a call to `POST /_matrix/client/v3/register`.
+56 -8
View File
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IContent, IEvent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { IEventWithRoomId, SearchKey } from "./search";
import { IRoomEventFilter } from "../filter";
import { Direction } from "../models/event-timeline";
import { PushRuleAction } from "./PushRules";
import { IRoomEvent } from "../sync-accumulator";
import { EventType, RelationType, RoomType } from "./event";
import { IContent, IEvent } from "../models/event.ts";
import { Preset, Visibility } from "./partials.ts";
import { IEventWithRoomId, SearchKey } from "./search.ts";
import { IRoomEventFilter } from "../filter.ts";
import { Direction } from "../models/event-timeline.ts";
import { PushRuleAction } from "./PushRules.ts";
import { IRoomEvent } from "../sync-accumulator.ts";
import { EventType, RelationType, RoomType } from "./event.ts";
// allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */
@@ -76,6 +76,54 @@ export interface ISendEventResponse {
event_id: string;
}
export type TimeoutDelay = {
delay: number;
};
export type ParentDelayId = {
parent_delay_id: string;
};
export type SendTimeoutDelayedEventRequestOpts = TimeoutDelay & Partial<ParentDelayId>;
export type SendActionDelayedEventRequestOpts = ParentDelayId;
export type SendDelayedEventRequestOpts = SendTimeoutDelayedEventRequestOpts | SendActionDelayedEventRequestOpts;
export type SendDelayedEventResponse = {
delay_id: string;
};
export enum UpdateDelayedEventAction {
Cancel = "cancel",
Restart = "restart",
Send = "send",
}
export type UpdateDelayedEventRequestOpts = SendDelayedEventResponse & {
action: UpdateDelayedEventAction;
};
type DelayedPartialTimelineEvent = {
room_id: string;
type: string;
content: IContent;
};
type DelayedPartialStateEvent = DelayedPartialTimelineEvent & {
state_key: string;
};
type DelayedPartialEvent = DelayedPartialTimelineEvent | DelayedPartialStateEvent;
export type DelayedEventInfo = {
delayed_events: (DelayedPartialEvent &
SendDelayedEventResponse &
SendDelayedEventRequestOpts & {
running_since: number;
})[];
next_batch?: string;
};
export interface IPresenceOpts {
// One of "online", "offline" or "unavailable"
presence: "online" | "offline" | "unavailable";
+3 -3
View File
@@ -16,9 +16,9 @@ limitations under the License.
// Types relating to the /search API
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
import { IRoomEventFilter } from "../filter";
import { SearchResult } from "../models/search-result";
import { IRoomEvent, IStateEvent } from "../sync-accumulator.ts";
import { IRoomEventFilter } from "../filter.ts";
import { SearchResult } from "../models/search-result.ts";
/* eslint-disable camelcase */
export interface IEventWithRoomId extends IRoomEvent {
+3 -3
View File
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPublicRoomsChunkRoom } from "../client";
import { RoomType } from "./event";
import { IStrippedState } from "../sync-accumulator";
import { IPublicRoomsChunkRoom } from "../client.ts";
import { RoomType } from "./event.ts";
import { IStrippedState } from "../sync-accumulator.ts";
// Types relating to Rooms of type `m.space` and related APIs
+4 -4
View File
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { RoomType } from "./event";
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "./partials";
import { ImageInfo } from "./media";
import { PolicyRecommendation } from "../models/invites-ignorer";
import { RoomType } from "./event.ts";
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "./partials.ts";
import { ImageInfo } from "./media.ts";
import { PolicyRecommendation } from "../models/invites-ignorer.ts";
export interface RoomCanonicalAliasEventContent {
alias?: string;
+1 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IdServerUnbindResult } from "./partials";
import { IdServerUnbindResult } from "./partials.ts";
// Types relating to Synapse Admin APIs
+1 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ServerControlledNamespacedValue } from "../NamespacedValue";
import { ServerControlledNamespacedValue } from "../NamespacedValue.ts";
/**
* https://github.com/matrix-org/matrix-doc/pull/3773

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