Compare commits
1173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14b2ee2da4 | |||
| bb083222d9 | |||
| fa424c44b4 | |||
| fef093747e | |||
| 4b33892d48 | |||
| d7d771fadb | |||
| 4ee3e591bf | |||
| 668183d722 | |||
| 854dae0dc0 | |||
| 9d1aca2232 | |||
| 81569f3461 | |||
| 50783aba76 | |||
| fd01b17236 | |||
| 25e92009b7 | |||
| eb7acfb810 | |||
| ca5655bced | |||
| ef9b13e2a6 | |||
| 159cca0363 | |||
| 3879111850 | |||
| f9a5aa87e3 | |||
| ed58df040c | |||
| 0a3448d4c9 | |||
| 25c1c1ea26 | |||
| a5e67af31f | |||
| b91e80814a | |||
| d096a72605 | |||
| fb547e7b4b | |||
| 815294ca5a | |||
| 6d270b4685 | |||
| 8bc3d96f6b | |||
| b6ea6e105e | |||
| cd4e053fa5 | |||
| 727473af62 | |||
| f17f013f1e | |||
| 9f4ab0b840 | |||
| 6371e4b252 | |||
| b69929e01a | |||
| 9dc12baaa9 | |||
| 159738597d | |||
| d02205652f | |||
| 5e03add29a | |||
| eeafd7fcaa | |||
| 78a3c5372d | |||
| c0c3bc2a8c | |||
| c3ce49cabf | |||
| 5408168dfd | |||
| 61452ddc11 | |||
| d5160a5380 | |||
| 7ff4960a27 | |||
| 00f63db80f | |||
| 9bcb83a20a | |||
| dd8d8e5410 | |||
| 32b8ff8116 | |||
| 3ceadd512d | |||
| 8182180550 | |||
| aed74c5a72 | |||
| 8c259c53a6 | |||
| 4d59291538 | |||
| 80009a1b31 | |||
| 93e6c95953 | |||
| 27a5507cef | |||
| be06f6655e | |||
| 71152f33bf | |||
| bc8f67089c | |||
| acc9aa8939 | |||
| e76f627fe3 | |||
| 45b1e73842 | |||
| f3eefd2f32 | |||
| f7c053216b | |||
| 9c7739f14f | |||
| 897afe153a | |||
| a929391dcd | |||
| 45c5ee9f65 | |||
| e56aaa16c7 | |||
| da0d3d791e | |||
| ed5eb670a1 | |||
| b7fcb6e4c1 | |||
| bd775f6b61 | |||
| c2f9ad28fc | |||
| 7f33e3462e | |||
| d99363d288 | |||
| 3642b99212 | |||
| 6ec0987286 | |||
| 219eb617dc | |||
| c6f9b25046 | |||
| 5d0e2efaf3 | |||
| c7cd5570d3 | |||
| c2f6dd2ce0 | |||
| 393732aaae | |||
| d373fd8540 | |||
| 44a8a9a47a | |||
| 8a0b7ad68b | |||
| 09663302e1 | |||
| 94f83b702c | |||
| 9df27ee672 | |||
| 5739b59faa | |||
| e4425570c7 | |||
| 145cb26054 | |||
| 26d5b1cde2 | |||
| 0666d6b4e1 | |||
| 9002064f10 | |||
| 5ea1554612 | |||
| bd6547c081 | |||
| 8073f27d98 | |||
| 3bb22a9b28 | |||
| ba8bb3228d | |||
| de23c9587b | |||
| 64eb482a49 | |||
| aba7f8a0d4 | |||
| 5495153c63 | |||
| 4f0696e2a4 | |||
| 0e659d294e | |||
| e74eb4928e | |||
| 327d2fa7c8 | |||
| 028357f15f | |||
| 872ec6755e | |||
| 333d6a7bd6 | |||
| 47532de452 | |||
| 87e1049dae | |||
| 4c8e38009f | |||
| 6e3efef0c5 | |||
| fb590627bb | |||
| 24cc17c270 | |||
| 68084e8fc3 | |||
| 0c3bb1f246 | |||
| 7f42b67f68 | |||
| 9b871ac969 | |||
| 49f7972a9e | |||
| c5ae4c8c0d | |||
| 2423300acd | |||
| 6cafa175b8 | |||
| 40165942e3 | |||
| f301251ff5 | |||
| 21cd5e98c1 | |||
| db070dca57 | |||
| 8b6ff0abcb | |||
| f2157f28bb | |||
| 739b8e1f89 | |||
| bc57a3f829 | |||
| f136f6ddf7 | |||
| d428e7119a | |||
| 5532066178 | |||
| 82b51d0d46 | |||
| fb12a5a1d6 | |||
| 61ea5a7dfc | |||
| 4e032317fe | |||
| dbb2ae5c07 | |||
| c8032a214e | |||
| 6e34ca6f2d | |||
| 4a7a699623 | |||
| 774776178e | |||
| ed4078528d | |||
| 7224961e9e | |||
| 35ca07e8fb | |||
| ccffb5df2d | |||
| 0d162f66f3 | |||
| bb7a689448 | |||
| c2b464a72c | |||
| 4a75d2c92f | |||
| 899cdb0e1d | |||
| da7c6717fe | |||
| b3fedf3a4e | |||
| 3d0ebdf6f1 | |||
| 25555ec431 | |||
| 33cd424f1f | |||
| 4d0d32307e | |||
| b1a578f62e | |||
| 841b654c00 | |||
| ca0d4622b3 | |||
| bfd87a0896 | |||
| 6c59b0c22f | |||
| eff75c6525 | |||
| ee4a0b001e | |||
| 5fcd6fd744 | |||
| bf58f18b5f | |||
| 0c020b3ca4 | |||
| 2727ebb67f | |||
| 8fae4f3111 | |||
| 455b614008 | |||
| 93f4f40202 | |||
| aeade9ce58 | |||
| 4b89fb23c5 | |||
| 174439c2f0 | |||
| 43f3e10f05 | |||
| 97fcdb2830 | |||
| 31e2d8eb20 | |||
| 633a5a8848 | |||
| a5086a09b9 | |||
| c251be9ae5 | |||
| ec137cb5fb | |||
| ab4e24f115 | |||
| 2218ec4e31 | |||
| 319a8309c5 | |||
| 5af046f54f | |||
| f97a9d9762 | |||
| dc1a57a9f2 | |||
| 2d6111a04b | |||
| 710fd7859d | |||
| e340a4ceaf | |||
| 3fa44e076e | |||
| 3f9fb9c936 | |||
| 8db347a75e | |||
| 8db3343280 | |||
| 25c5a5b4ff | |||
| a696e77652 | |||
| 582a76d87c | |||
| fdfddde55a | |||
| 0ecfef2352 | |||
| 4fdece6c1c | |||
| 6f0bce8708 | |||
| d3bdeb73f5 | |||
| 942fdf5bee | |||
| dd2635dbe6 | |||
| 3d1bcb73c1 | |||
| a960e686b3 | |||
| 946774c3fb | |||
| 15edbc8067 | |||
| 1398ac24a2 | |||
| c76df4cd8f | |||
| a5e4dbf2d3 | |||
| 3768187395 | |||
| 08d0ce25f1 | |||
| 23241f18e2 | |||
| 90da67aa95 | |||
| 0bf2702149 | |||
| c7a75c8824 | |||
| 98b2b9745d | |||
| 65d5b3172c | |||
| 2f72f9e889 | |||
| 18f500a1f8 | |||
| b1df58796a | |||
| 761b3771d6 | |||
| df88edfda0 | |||
| 1dee1ba581 | |||
| b274c74a30 | |||
| b489bb15cf | |||
| dff4922a42 | |||
| dc6ad0b54c | |||
| 9769c05dc5 | |||
| dd379d3d4c | |||
| 1b884a3e52 | |||
| ddb164490e | |||
| 796135c7ce | |||
| 4cc4c01dd8 | |||
| 533b40922c | |||
| 0ae483ce27 | |||
| dbc1fa87ed | |||
| 0a3675b971 | |||
| ab3f529d29 | |||
| 607b712a07 | |||
| b6d9e49277 | |||
| 731d5943e2 | |||
| 01e7a43593 | |||
| b69a19ce7c | |||
| 8703acb533 | |||
| b59603d748 | |||
| b0cbe22f64 | |||
| 977d0322da | |||
| dd7394c14c | |||
| f2d082064e | |||
| 2731e20893 | |||
| 502a513b5b | |||
| 3ac47e71cd | |||
| 7c1e25e713 | |||
| 2d90ad95f1 | |||
| cd9794471f | |||
| b2d3ab8bc1 | |||
| d8b70ef83b | |||
| a67fb1fb8d | |||
| ddd6e77cde | |||
| 2e9f5b6033 | |||
| fd949fe486 | |||
| 7b3aed8a47 | |||
| b84a73c7cc | |||
| a03cf054a8 | |||
| b3d217717a | |||
| 3e6fe5f914 | |||
| a213d177f9 | |||
| e885ecf08d | |||
| d1d9aba745 | |||
| 52bcc2c955 | |||
| 1994806c72 | |||
| c4d1fd2c67 | |||
| 7ad8288525 | |||
| 31a42964e6 | |||
| 2b1d37813c | |||
| e6fd0c58ff | |||
| 41d70d0b5d | |||
| a08a2737e1 | |||
| dbe441de33 | |||
| 9f3ca71495 | |||
| ef97df8ed0 | |||
| 7f74fcc9f7 | |||
| e39644ad08 | |||
| df0f0074b4 | |||
| 29fbed5603 | |||
| 5ee6fc196b | |||
| 961e32a3bb | |||
| 25f0418fce | |||
| 0ce751c462 | |||
| b7b3588cb8 | |||
| 72846c713d | |||
| 7c5229b4c8 | |||
| dd08388397 | |||
| ec1ccebcca | |||
| e9b45cc504 | |||
| 5d4df65c09 | |||
| 2706873948 | |||
| 4a9006aea6 | |||
| 43c72d5bf5 | |||
| e551b92a07 | |||
| 32f51e852b | |||
| 82aa04d894 | |||
| ff89c9ec42 | |||
| ccd825fb39 | |||
| b313eb5912 | |||
| 2b12675675 | |||
| 246788b874 | |||
| b0b80401aa | |||
| 6a0164f37f | |||
| f963d61bcb | |||
| b32619ad24 | |||
| 3d3c3ba55f | |||
| d62c658a72 | |||
| bdc4a69023 | |||
| ab892420b5 | |||
| ed607c48b0 | |||
| a8d75b81e5 | |||
| 2f1d654f14 | |||
| c4c7f94514 | |||
| 1fac06e223 | |||
| 77c118084b | |||
| 097bfe451a | |||
| b80d0091d2 | |||
| 3a33c658bb | |||
| 81e42b9531 | |||
| 7f7ecd060d | |||
| 6126ee125a | |||
| 78f718ff82 | |||
| c1c1be0c5d | |||
| 1952eaa1ff | |||
| 8851f8b07c | |||
| 15eafe34b3 | |||
| f0d48236fa | |||
| dde9d48726 | |||
| 6d046edcb2 | |||
| 2abf7ca795 | |||
| 2b46579bd8 | |||
| 6d42ed338e | |||
| ef080c25f9 | |||
| c8d7b458b2 | |||
| f1ba8a8775 | |||
| d21adf568a | |||
| dea184e9ec | |||
| e119bf9040 | |||
| c7f982e190 | |||
| 2e2dd628c1 | |||
| 5ac5a8a799 | |||
| 7d75ab417a | |||
| 3ca81e409a | |||
| 764fdb1d30 | |||
| c2d25d9377 | |||
| c4e1e0723e | |||
| 56b24c0bdc | |||
| aa7a709e3f | |||
| c57c47319e | |||
| 812d0aaef6 | |||
| 61e07633df | |||
| c7dbd6e33b | |||
| 556494b8f0 | |||
| bf3b4e81b2 | |||
| 2710600389 | |||
| ca168c494b | |||
| 759f5ed3eb | |||
| 53deedd2d6 | |||
| 4fbb9f92a7 | |||
| 1a58ce4649 | |||
| 32509d1fd1 | |||
| 11c9e39e5a | |||
| 0ce944f3da | |||
| 38e04c8fb0 | |||
| 1fcbc6ebeb | |||
| aa79236ce2 | |||
| f8f1bf3837 | |||
| 946cf4f359 | |||
| c077201f2a | |||
| 53f2ad41d6 | |||
| be15a709c6 | |||
| 090b8079db | |||
| 06a1e1a88a | |||
| 024b62bba0 | |||
| 6bfb911cf7 | |||
| ad5da7cfa1 | |||
| e55bd1e14c | |||
| 119e859741 | |||
| b7cce93edc | |||
| e5c8c20a34 | |||
| 137379b7b7 | |||
| 4981efa54b | |||
| 6e9740d787 | |||
| 70257e0ab4 | |||
| 9baba151c6 | |||
| b4672e26ec | |||
| 940d358b0e | |||
| 161c12f5d5 | |||
| de659d6431 | |||
| b095aa600d | |||
| 18ea8befdc | |||
| 4f9ca2c697 | |||
| 57a4dc8841 | |||
| 841c02e56d | |||
| 6ece4c3c16 | |||
| 5a3c07f91d | |||
| 3ed4b3ed50 | |||
| 9ca9bd9baf | |||
| 0265f6ea2d | |||
| 8f597f0f87 | |||
| 67df2a53c4 | |||
| 8367277894 | |||
| 4efb27354f | |||
| 28bc90563e | |||
| f9be1bf57a | |||
| adaf921623 | |||
| cdece6cb9f | |||
| d438e25f87 | |||
| 73d8f4384d | |||
| b0cb6aa724 | |||
| dfc26f8aa1 | |||
| 286fb8f752 | |||
| 921de6807d | |||
| d4e7b949e7 | |||
| 40bc833bb7 | |||
| a46ad75440 | |||
| 1e80538cfb | |||
| 73cbcfa4ee | |||
| 99972ce0a9 | |||
| 44399f6017 | |||
| c387f30e5c | |||
| eb9867a5ba | |||
| 12a9875c46 | |||
| 74f5efc4ef | |||
| 43d47982ed | |||
| 94fb489952 | |||
| 75ae05e5eb | |||
| 9058b79c39 | |||
| 671dd2ca40 | |||
| 34f35393ff | |||
| e206a12902 | |||
| 76eabb2efa | |||
| bf6dc16ad3 | |||
| 9398271695 | |||
| ef7a818f70 | |||
| b8903ddf3e | |||
| c35c7d1a3b | |||
| 286e00c500 | |||
| 26922a61f3 | |||
| 457a300c95 | |||
| be04f003ce | |||
| 67b445d40d | |||
| 27f28d5558 | |||
| 52bac9648b | |||
| 7e0e5a3243 | |||
| 337d1791cf | |||
| 622998e949 | |||
| 673806ecaa | |||
| b2232289a6 | |||
| 344c8fad9d | |||
| cccaaf6e56 | |||
| 4dbca983b4 | |||
| 54e8f3c9d0 | |||
| 1fcc375dd5 | |||
| d24c5d8b2b | |||
| bb9280ad6b | |||
| 00bd7f0f02 | |||
| a29b8736f3 | |||
| 1c8a1cd5a1 | |||
| fea619d34c | |||
| f322f32a07 | |||
| bc3abd4394 | |||
| 6721aca293 | |||
| 80b51c0e30 | |||
| 91aa1dc092 | |||
| 78972e81d1 | |||
| 663136a95a | |||
| d67b19fa88 | |||
| 6ec200adcf | |||
| 19b1b901f5 | |||
| 1f52fa0c43 | |||
| b08c083c46 | |||
| 69f1bea89b | |||
| a52c64b747 | |||
| de9eb0e7f2 | |||
| a97d7f1133 | |||
| bc399abf6e | |||
| 1085a2469a | |||
| 52a9c52b12 | |||
| ac49137c0e | |||
| a179227bf1 | |||
| 64e27f5d3c | |||
| 634651859b | |||
| 480c8e86a4 | |||
| 1ba4412260 | |||
| e3a3a52f2a | |||
| 3f03c1da89 | |||
| eb793aaa08 | |||
| f7bab544a7 | |||
| b3fd92ad16 | |||
| ba71235539 | |||
| d6ede767c9 | |||
| 43f388d89a | |||
| 1d46b182c6 | |||
| 352f6986cd | |||
| 0ca72ec466 | |||
| 375f086f44 | |||
| 5976083e32 | |||
| d53d2dff45 | |||
| 3657eb61d8 | |||
| 1def88eb35 | |||
| 6f9e68c8f3 | |||
| 5a65c8436d | |||
| 2090319bdd | |||
| f4de8837fd | |||
| 1e92c13a75 | |||
| 8061fa924d | |||
| 0760c5f64c | |||
| e2bdb2f057 | |||
| fd47a189e0 | |||
| c233334f27 | |||
| 029ab5f109 | |||
| 366f686827 | |||
| 26387287af | |||
| ba0bbe1cba | |||
| 3c37d7b0fb | |||
| 097d645c19 | |||
| e62aabccd9 | |||
| 0f3bcf3736 | |||
| 8395919f0f | |||
| 6cdc68d19d | |||
| c879258545 | |||
| b14cc82682 | |||
| 9f9be701e7 | |||
| f552370c26 | |||
| 5f3f08071f | |||
| db7e3e3cf3 | |||
| 3e512711d7 | |||
| f636976f15 | |||
| 905df2f7ca | |||
| c1c473e6c6 | |||
| 4835ca61ec | |||
| abdc8b2c2e | |||
| 89fe90d27c | |||
| 86e0eb11cc | |||
| df7f69b6a2 | |||
| 71bffb6c1b | |||
| 72b997d1f3 | |||
| a3bbc49e02 | |||
| d81929de4c | |||
| 27002a0d8f | |||
| bb2a0bec8c | |||
| 2df8876f60 | |||
| 502f0ec0eb | |||
| 1ddba3460f | |||
| 524cb65c5d | |||
| ea770282ea | |||
| bcaa7f63c7 | |||
| 8ab9025282 | |||
| e49a0a5013 | |||
| 5b939287cc | |||
| 2d381ade22 | |||
| 266475c37d | |||
| 597db17e59 | |||
| 9ea5b512e6 | |||
| 2ce57fd942 | |||
| 102fa91efe | |||
| 266e9257aa | |||
| f7015b35a0 | |||
| 26caa90805 | |||
| 1892dc13e0 | |||
| 0efeac9b3e | |||
| f11e1910f5 | |||
| a1a0463229 | |||
| 30aa66e680 | |||
| b584f818f5 | |||
| 4d7616bd68 | |||
| 554804cd10 | |||
| 33648a711c | |||
| c537a361fb | |||
| 30d9b0518f | |||
| 900c782216 | |||
| 810f7142e6 | |||
| 6d7699cb4a | |||
| 6b93e11e2c | |||
| ff1db2b538 | |||
| 6e72b3554e | |||
| 7d687533e7 | |||
| 432c4eceb9 | |||
| 466ad3ef02 | |||
| 5dd213b790 | |||
| bdc3513e7a | |||
| c520f70e92 | |||
| 25c8c6aaf4 | |||
| 53610fa5a7 | |||
| ea34cce00a | |||
| 6b0d2a4ad3 | |||
| 72519a0eb4 | |||
| 624b8a242e | |||
| 5be104a35c | |||
| c93128ed39 | |||
| cc238c24ab | |||
| 8175683d4b | |||
| 4e54b7aab4 | |||
| a2fd06bdf9 | |||
| 7d8cbd6ef0 | |||
| 44158bc843 | |||
| e4590cac94 | |||
| fd9a44e701 | |||
| df492800b4 | |||
| 161da05972 | |||
| ed397d99ed | |||
| c0e30ceca0 | |||
| 61375ef38a | |||
| e5fda72884 | |||
| acfc619f31 | |||
| 7a0af07bf7 | |||
| ad1afbc45b | |||
| b4ce4ce184 | |||
| ddbc66b905 | |||
| ad1c67e33e | |||
| d473e16df7 | |||
| 806538828e | |||
| 424c258a07 | |||
| ea67d3977e | |||
| 8bffa39eb9 | |||
| f13967aaec | |||
| b496601712 | |||
| ce60162827 | |||
| 1f1d6f0bc8 | |||
| 35fe7bc60a | |||
| b45d51a131 | |||
| a6fd28b03d | |||
| 78e7e2af31 | |||
| 86494c3a96 | |||
| bdd4d82cb3 | |||
| 5babcaf4b3 | |||
| ffbb4716c4 | |||
| 07f97d724f | |||
| 72ee5504d5 | |||
| 9134471dc7 | |||
| f22d5e9d47 | |||
| ffb228bf5a | |||
| bed4e9579e | |||
| 7697338c7e | |||
| 1da26b5cd1 | |||
| 5b85ae491e | |||
| 2024b070b0 | |||
| eef964f07d | |||
| 1e9c119159 | |||
| fd894309f2 | |||
| 75486b72a6 | |||
| 38816589f5 | |||
| 6f743bfa1f | |||
| ffd3c9575e | |||
| 7678923e04 | |||
| 6f7c74f9ea | |||
| 3fcc56601b | |||
| bcf3d56bd5 | |||
| 647a2a1a19 | |||
| 1bf8533c03 | |||
| fb2f2dd6d4 | |||
| d33350ff26 | |||
| 349a86c119 | |||
| bee65ff13f | |||
| e4182eb752 | |||
| 3219aefc92 | |||
| aba4e690af | |||
| 693bb22ba1 | |||
| 315e81b7de | |||
| a0502c5ee5 | |||
| d1de32ea27 | |||
| 3ae25427a8 | |||
| 8155b0acfc | |||
| 413c156624 | |||
| e78a3cec9f | |||
| 5998de365d | |||
| 3de2b9bf80 | |||
| ded87290ce | |||
| cf39595bd7 | |||
| c90ea985c6 | |||
| c54ca29aa8 | |||
| beb3721e7a | |||
| 1cad6f4451 | |||
| c4ea57d42d | |||
| d3f5526ec0 | |||
| b8e332b53d | |||
| ab78acc7da | |||
| 051f4e2ab9 | |||
| 8863e42e35 | |||
| bc5246970c | |||
| edac6a9983 | |||
| 97ef1dc6df | |||
| 125e45c24d | |||
| 3781b6ebfa | |||
| 8fc77c595a | |||
| 5bcd26e506 | |||
| 66f099b2e7 | |||
| b1445d7457 | |||
| b3794760c6 | |||
| fbdcc597b6 | |||
| 8ac32b7894 | |||
| b27c6de78d | |||
| c8e0987774 | |||
| 849fcd3341 | |||
| 8df8266e4a | |||
| cba4edfd84 | |||
| 4f50290cd6 | |||
| 35e31f02ac | |||
| 69647a33b6 | |||
| 13df897896 | |||
| 006929ac0c | |||
| 405a6fbb92 | |||
| d6f3262e12 | |||
| 8b32f3eb7f | |||
| 1e9934a69d | |||
| a6d342d3a4 | |||
| 4b9a1bd53f | |||
| 26248f85d5 | |||
| ae2ae483db | |||
| c87692d0aa | |||
| 2dd4334e20 | |||
| 2210255d6f | |||
| 544ac86d20 | |||
| cf06547063 | |||
| 781c3b05e5 | |||
| 325dace437 | |||
| 5c894b34b3 | |||
| f5f4091a00 | |||
| 52bdb57a47 | |||
| 3c23eb69b5 | |||
| c93b7ce188 | |||
| 705b6336cf | |||
| 7c41e3fb06 | |||
| a314e612fa | |||
| ac6cad2852 | |||
| 906390f0bf | |||
| 795497fafa | |||
| ffb777d118 | |||
| fbba8a2d71 | |||
| 053c5741b0 | |||
| 620dc2f6e2 | |||
| 00d78077b0 | |||
| 23d2d03912 | |||
| b4c4355d1a | |||
| 142c0a65e6 | |||
| b9aacea1cb | |||
| 76e653b7ee | |||
| 4d4ff4c3f2 | |||
| 00aba742e4 | |||
| 35d862ebd3 | |||
| 581b3209ab | |||
| 6855ace642 | |||
| 5033d48013 | |||
| 4c53836a13 | |||
| 10a4fd8328 | |||
| 98f7637683 | |||
| 0df8e81da4 | |||
| 30b1894f37 | |||
| fbbdb6e766 | |||
| 5a1488ebd5 | |||
| c4048d985d | |||
| 794f044dda | |||
| a197afe8aa | |||
| 1061b93b29 | |||
| 6528a59fc1 | |||
| f6a169b5a5 | |||
| d04135cc1c | |||
| 546047a050 | |||
| 16153e5d82 | |||
| fd73d5068c | |||
| e72859a44a | |||
| a8f8a9c14d | |||
| 0e2f73d7a7 | |||
| 31aeb3044f | |||
| 0a29063bc9 | |||
| 3cc3bd0728 | |||
| f891fe4423 | |||
| b99ff83785 | |||
| 23c4c9fd8a | |||
| 8b8ee91210 | |||
| d8c431f23e | |||
| a6fb7530cb | |||
| b5c3f15a67 | |||
| 91f6f0f9c5 | |||
| 88cf5eb769 | |||
| 13a967ae8f | |||
| 66c80949e8 | |||
| 3d51e31da8 | |||
| 86c190fa0d | |||
| e9a9280e3c | |||
| fabdd6da64 | |||
| ab5d95102c | |||
| b4556d6552 | |||
| 6c22da9a96 | |||
| d29329bddb | |||
| 68b06d7b60 | |||
| 5f599ee978 | |||
| aabd558bce | |||
| 662b772c73 | |||
| b240c44128 | |||
| 5508993d79 | |||
| c9b43ab251 | |||
| 2fb1e659c8 | |||
| b842192a34 | |||
| 868bbfcb31 | |||
| 860161bdd2 | |||
| 0c9d82e40a | |||
| 73ab7c342a | |||
| 3386c66b98 | |||
| da044820d7 | |||
| 9f40f32b3e | |||
| f679942584 | |||
| c1b4f97372 | |||
| 5f3b89990d | |||
| 866fd6f4a3 | |||
| 9ecb66e695 | |||
| baa6d13506 | |||
| 2d6230f199 | |||
| 825d85f18d | |||
| 1dcb7a6e75 | |||
| 823316b2ff | |||
| d56fa197d0 | |||
| f7229bfff0 | |||
| 538717c23e | |||
| 1a8ea3d685 | |||
| d0890d9450 | |||
| 42ec17b977 | |||
| 82e9eefce6 | |||
| f8208b1891 | |||
| 092a59af66 | |||
| dbd7d26968 | |||
| 4fda9e8419 | |||
| a13e0389db | |||
| dbb4828eda | |||
| 414ac9d8cc | |||
| 30058a4bdc | |||
| fab9cab3df | |||
| 53b599f8fe | |||
| 17f6cc733e | |||
| c8403f39aa | |||
| 8cf5df73ee | |||
| febe27ddcc | |||
| 7987ce76ec | |||
| 60cedf2fdb | |||
| ed44514974 | |||
| 9f8c1ee953 | |||
| 593a57fc2b | |||
| e8128d34a1 | |||
| ba7bd06295 | |||
| e4db6008b8 | |||
| 52f35409ec | |||
| f50aab37c3 | |||
| df0f817f83 | |||
| 7efa6352f8 | |||
| f74614705e | |||
| 169e8f8613 | |||
| f2f77bd1f7 | |||
| 5a1c70ad19 | |||
| 27cb16ffe4 | |||
| 9be0b3e701 | |||
| 05ba27f36b | |||
| e6acfdf275 | |||
| 2a6612c73a | |||
| 2f8b05b0da | |||
| fe984ede6e | |||
| 3f74b9a0cc | |||
| 802b996b10 | |||
| 8d44f9d665 | |||
| a72a1b294a | |||
| ab5f32f984 | |||
| 6a21d812ab | |||
| ee94e93354 | |||
| 31c4786a96 | |||
| 17b6e59819 | |||
| 42510022a1 | |||
| 5f0978ac3f | |||
| cd6787e0ac | |||
| 03baa3e358 | |||
| 658563e2a7 | |||
| 26d3033b17 | |||
| a4bd7dc7d7 | |||
| 1f48544b38 | |||
| 7ef4062f59 | |||
| 968bc51a35 | |||
| d413f5042e | |||
| b8e8b14375 | |||
| 43e58871de | |||
| 2544c14032 | |||
| 8d19782c57 | |||
| 340bbe1a8f | |||
| 8214fd7156 | |||
| a0efed8b88 | |||
| c408c0d1d5 | |||
| d6080398db | |||
| 467908703b | |||
| 87eddaf51a | |||
| c65ef03567 | |||
| 78cbf7cd28 | |||
| dc7c1a4fef | |||
| 1ae0c2f3ee | |||
| affaa95fb4 | |||
| 9176d3a671 | |||
| de50129a53 | |||
| 5568dfdd41 | |||
| 39216d44ed | |||
| 8c3b249567 | |||
| b8e40ad2a8 | |||
| 4e2831764d | |||
| 09780672aa | |||
| 0fe53876ec | |||
| dfec3dc33c | |||
| fbdd78b428 | |||
| e10c362ef0 | |||
| 89a9a7fa38 | |||
| 687d08dc9d | |||
| 7f91db83d0 | |||
| 0300d6343f | |||
| e0ef467d7d | |||
| dc1cccfecc | |||
| 79299891fd | |||
| d32f398345 | |||
| 0f08c00c07 | |||
| 6b261b98c9 | |||
| 99f157a0f1 | |||
| f9f6d81346 | |||
| 46604abe7b | |||
| 553758e713 | |||
| 509e64cfc1 | |||
| 60c2e9b3ed | |||
| cfb21fa80a | |||
| c3d7f4e730 | |||
| aa97beae44 | |||
| 5feab37166 | |||
| 1a02835ab2 | |||
| 6f63ff1711 | |||
| 4d90fecb6a | |||
| 30a26813ec | |||
| f17a4fedb9 | |||
| 94e393c9a6 | |||
| 53201688a6 | |||
| 996663bf64 | |||
| d6e4338a37 | |||
| b2665f2128 | |||
| af4b6bc126 | |||
| 565bb0ef7c | |||
| fe0edcd081 | |||
| 6520e0f54f | |||
| ed7b314e6a | |||
| 24eff501e4 | |||
| 51544f25a7 | |||
| a0d73dfaca | |||
| 5d2500b7a7 | |||
| eff52b82e8 | |||
| 2868308079 | |||
| a5ef569717 | |||
| c06b22ae7c | |||
| 7a51798acb | |||
| 712ba617de | |||
| 957329b218 | |||
| 1733ec7b7f | |||
| 24c589923b | |||
| 03ed4f5dd7 | |||
| 6e641a28c0 | |||
| 1586de44bd | |||
| b36682cb99 | |||
| 04ea2a4e5d | |||
| b71099d0f8 | |||
| ccc2fb5663 | |||
| 0a7f7efd9d | |||
| ae58d0c8eb | |||
| 20a6704497 | |||
| 3337bda752 | |||
| d90292bff5 | |||
| 3de0c02757 | |||
| 65b9c31f9b | |||
| d629a685c2 | |||
| 0210106be2 | |||
| 3e05a71068 | |||
| c755810d9c | |||
| 2d492f60a0 | |||
| 16db2c5f9a | |||
| 29c02d8c37 | |||
| 0f98df158c | |||
| 2ea4ce0bb6 | |||
| 3e0017fecf | |||
| a0073ddaaf | |||
| c29e116c0c | |||
| d9f372ca79 | |||
| 6417f4fac7 | |||
| 4bae83f59f | |||
| b8c68eb102 | |||
| 8790cde6d4 | |||
| ab6260074d | |||
| 25a7c9e140 | |||
| 78b6b878bd | |||
| 4ccb72c0f2 | |||
| 9f1aebbdcb | |||
| 6a15e8f1a0 | |||
| 238eea0ef5 | |||
| ab6f86536f | |||
| 819fc75202 | |||
| c70aa33367 | |||
| 240b43b652 | |||
| 697d5d31d1 | |||
| b1701ff571 | |||
| c55289ec65 | |||
| 987ec1e62f | |||
| 76b240cf57 | |||
| a4c4e7e275 | |||
| 3f5a994a24 | |||
| d754392410 | |||
| 7ecaa53e34 | |||
| 222e95d33f | |||
| 2ee43cade7 | |||
| 9218f6380c | |||
| 661ba76763 | |||
| 4cb851c51a | |||
| 5a3d24abc2 | |||
| 3eed74f1a6 | |||
| 10e7a2d997 | |||
| 969ecdb6fb | |||
| e8b91f2729 | |||
| f80366ff30 | |||
| 395c3cfcd6 | |||
| f95954c233 | |||
| fa5f2d389a | |||
| 9fc557fc6b | |||
| 6436fbb99f | |||
| 87c2ac3ffa | |||
| 43022d5b2f | |||
| a0fadeb4ec | |||
| a3cea8ce7d | |||
| c88487da07 | |||
| 89875b8e31 | |||
| 9c94393d76 | |||
| 7850294a4b | |||
| 131e81401a | |||
| 5c27e30302 | |||
| 8dfb6de3cc | |||
| 042610310f | |||
| 8535604200 | |||
| 3ee64722c5 | |||
| 9d6210b3f9 | |||
| 909caab74e | |||
| 7c87625157 | |||
| b19817bb73 | |||
| 36196ea422 | |||
| a81adf542e | |||
| a49bc3ddf4 | |||
| a86d4ceb49 | |||
| 8c3be2a56a | |||
| 38898a60c7 | |||
| fd3a4d4403 | |||
| 93d96281fd | |||
| 944dc51c58 | |||
| c6b43dd176 | |||
| f03dd7b7bc | |||
| 51fa1866a9 | |||
| d76fb2baa0 | |||
| 3feafc9c17 | |||
| c9075b3dba | |||
| 69c474dda7 | |||
| 73ce51065f | |||
| 5d0407d0a6 | |||
| 3a4b02d8e6 | |||
| d421e7f829 | |||
| 9fd051af33 | |||
| b78a1ad889 | |||
| a25cdcecaa | |||
| 2a716bd076 | |||
| ef1db8d664 | |||
| 22865fd834 | |||
| c4fe564855 | |||
| 9ecb1a0381 | |||
| ef9490c7b1 | |||
| 402adfbe8a | |||
| 41e8c2af34 | |||
| 4843b40296 | |||
| bc2c870152 | |||
| 7c7b2817d3 | |||
| 9f78202ecd | |||
| abc9911e95 | |||
| efdae0d66f | |||
| 2bf554761c | |||
| c2687643b5 | |||
| a33758eda6 | |||
| 8faed02cc5 | |||
| 3ae0dab47a | |||
| 95394e4cbe | |||
| 8c9bbc01fc | |||
| eb888791a3 | |||
| 6c0b2f55e1 | |||
| c9a5eaece3 | |||
| 64505de36b | |||
| 65d858f9a3 | |||
| 1da5e8f56a | |||
| 5efd4c2915 | |||
| bc03950f8a | |||
| c09da9a23f | |||
| e874468ba3 | |||
| 6fedda91f9 | |||
| d22a39f5d7 | |||
| 4fc6ba884e | |||
| c30e498013 | |||
| 8240bf0ae7 | |||
| 2321c44687 | |||
| 0137e9d5a8 | |||
| 28bbc51752 | |||
| 0db3ac9b43 | |||
| 53039b78ee | |||
| 2a06d19431 | |||
| a747eef04c | |||
| 583823c2ef | |||
| 26d13c15c3 | |||
| c850ca3179 | |||
| 8438533532 | |||
| 475f82c5ce | |||
| 936e7c3072 | |||
| 82ed7bd86a | |||
| cb67eae858 | |||
| e4937e6222 | |||
| 5cdd524da7 | |||
| 0ff0093380 | |||
| b352405c89 | |||
| 0d73d0c6c7 | |||
| d2f76d4956 | |||
| c680dd7eb2 | |||
| e24bb0f50c | |||
| 1ed3b13f0d | |||
| 4f628bf64c | |||
| 7d5c003716 | |||
| dbab185f9d | |||
| cfcd191cbf | |||
| 514633c5fa | |||
| 78a225795b | |||
| 467b49a0dc | |||
| 06e083874a | |||
| 0f25429849 | |||
| 32ddf2813d | |||
| 1ed082f3d4 | |||
| 706002cdcb | |||
| 731de1108c | |||
| 9f1d0c3896 | |||
| 0b290fffa1 | |||
| 97844f0e47 | |||
| 85a55c79cd | |||
| 63d4195453 | |||
| d5a35f8a99 | |||
| d1259b241c | |||
| a573727662 | |||
| dce8acbf17 | |||
| 4ba1341f8f | |||
| e517d009bf | |||
| dc2d03dea5 | |||
| d5bb9e7600 | |||
| d908036f50 | |||
| afc3c6213b | |||
| 7884c22e41 | |||
| 887d8a7663 | |||
| 2c68ee2254 | |||
| d445823d0b | |||
| abe4630687 | |||
| 8664b66238 | |||
| 596826ab4d | |||
| c8ec5421c7 |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
@@ -49,6 +49,26 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
object: "window",
|
||||
property: "setImmediate",
|
||||
message: "Use setTimeout instead.",
|
||||
},
|
||||
],
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "setImmediate",
|
||||
message: "Use setTimeout instead.",
|
||||
},
|
||||
{
|
||||
name: "global",
|
||||
message: "Use globalThis instead.",
|
||||
},
|
||||
],
|
||||
|
||||
"import/no-restricted-paths": [
|
||||
"error",
|
||||
{
|
||||
@@ -63,17 +83,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"jest/no-disabled-tests": "off",
|
||||
// Also treat "oldBackendOnly" as a test function.
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -92,10 +101,13 @@ module.exports = {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
"@typescript-eslint/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
// We do this sometimes to brand interfaces
|
||||
allowInterfaces: "with-single-extends",
|
||||
},
|
||||
],
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
@@ -112,14 +124,61 @@ 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"],
|
||||
},
|
||||
],
|
||||
"no-extra-boolean-cast": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["spec/**/*.ts"],
|
||||
extends: ["plugin:@vitest/legacy-recommended"],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"@vitest/no-disabled-tests": "off",
|
||||
// Used in some crypto tests.
|
||||
"@vitest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
|
||||
},
|
||||
],
|
||||
"@vitest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
assertFunctionNames: [
|
||||
"expect",
|
||||
"expectDevices",
|
||||
"assert.isTrue",
|
||||
"assert.isFalse",
|
||||
"passwordTest",
|
||||
"compareHeaders",
|
||||
"doTest",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enable stricter promise rules for the MatrixRTC codebase
|
||||
files: ["src/matrixrtc/**/*.ts", "spec/unit/matrixrtc/*.ts"],
|
||||
rules: {
|
||||
// Encourage proper usage of Promises:
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
},
|
||||
},
|
||||
],
|
||||
+3
-1
@@ -1,12 +1,14 @@
|
||||
* @matrix-org/element-web-reviewers
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
/pnpm-lock.yaml @matrix-org/element-web-team
|
||||
/scripts/** @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
/src/crypto-api @matrix-org/element-crypto-web-reviewers
|
||||
/src/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/src/rust-crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/integ/crypto @matrix-org/element-crypto-web-reviewers
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
|
||||
@@ -29,13 +29,13 @@ runs:
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
- 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"
|
||||
- name: "Z-Skip-Coverage"
|
||||
description: "Skip SonarQube coverage for this PR"
|
||||
color: "ededed"
|
||||
@@ -1,16 +1,20 @@
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
# Privilege escalation necessary to enable backporting PRs from forks
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
|
||||
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,26 +1,31 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to publish to Netlify
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Static Analysis"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
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
|
||||
permissions:
|
||||
actions: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v3
|
||||
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
||||
with:
|
||||
path: docs
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Build downstream artifacts
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
pull_request: {}
|
||||
|
||||
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
|
||||
# to develop or master.
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.93.0
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
@@ -1,13 +1,19 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# matrix-react-sdk playwright tests (with access to repo secrets)
|
||||
# element-web playwright tests (with access to repo secrets)
|
||||
|
||||
name: matrix-react-sdk End to End Tests
|
||||
name: Element Web End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build downstream artifacts"]
|
||||
types:
|
||||
- completed
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
pull_request: {}
|
||||
|
||||
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
|
||||
# to develop or master.
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
permissions: {} # No permissions required
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
@@ -15,44 +21,14 @@ concurrency:
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
if: github.event.workflow_run.event == 'merge_group'
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
|
||||
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
deployments: write
|
||||
contents: read
|
||||
with:
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
# We want to make the Playwright tests a required check for the merge queue.
|
||||
#
|
||||
# Unfortunately, GitHub doesn't distinguish between "checks needed for branch
|
||||
# protection" (ie, the things that must pass before the PR will even be added
|
||||
# to the merge queue) and "checks needed in the merge queue". We just have to add
|
||||
# the check to the branch protection list.
|
||||
#
|
||||
# Ergo, if we know we're not going to run the Playwright tests, we need to add a
|
||||
# passing status check manually.
|
||||
mark_skipped:
|
||||
if: github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
state: success
|
||||
description: Playwright skipped
|
||||
|
||||
# Keep in step with the `context` that is updated by `Sibz/github-status-action`
|
||||
# in matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml.
|
||||
context: "${{ github.workflow }} / end-to-end-tests"
|
||||
|
||||
sha: "${{ github.event.workflow_run.head_sha }}"
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
skip: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches: [develop]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
@@ -12,15 +13,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
- repo: element-hq/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
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
|
||||
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
# Privilege escalation necessary access members of the review teams
|
||||
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
|
||||
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
@@ -9,12 +12,13 @@ on:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@80a96a4863886addcbc9f681b5b295ba7f5424e1 # v5
|
||||
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
@@ -29,12 +33,12 @@ jobs:
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
@@ -42,11 +46,13 @@ jobs:
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event.action == 'opened'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check membership
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]'
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
|
||||
id: teams
|
||||
with:
|
||||
@@ -57,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Add label
|
||||
if: steps.teams.outputs.isTeamMember == 'false'
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
@@ -69,14 +75,16 @@ jobs:
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Release Sanity checks
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: false
|
||||
inputs:
|
||||
repository:
|
||||
type: string
|
||||
required: false
|
||||
default: ${{ github.repository }}
|
||||
description: "The repository (in form owner/repo) to check for release blockers"
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
REPO: ${{ inputs.repository }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { REPO } = process.env;
|
||||
const { data } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${REPO} label:X-Release-Blocker is:open`,
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
if (data.total_count) {
|
||||
data.items.forEach(item => {
|
||||
core.error(`Release blocker: ${item.html_url}`);
|
||||
});
|
||||
core.setFailed(`Found release blockers!`);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Release Drafter
|
||||
# Workflow used by other workflows to generate draft releases.
|
||||
name: Release Drafter Reusable
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -7,23 +8,28 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
concurrency: release-drafter-action
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||
id: draft-release
|
||||
@@ -33,7 +39,7 @@ jobs:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -44,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: inputs.include-changes
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
@@ -55,7 +61,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("/")) {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
# Generates the draft release for the js-sdk
|
||||
# Normally triggered whenever anything is merged to the staging branch, but
|
||||
# also has a workflow dispatch trigger in case it needs running manually due
|
||||
# to failures / workflow updates etc.
|
||||
name: Release Drafter
|
||||
on:
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch: {}
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
||||
permissions:
|
||||
contents: write
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
|
||||
@@ -12,18 +12,25 @@ on:
|
||||
description: List of dependencies to reset.
|
||||
type: string
|
||||
required: false
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -31,12 +38,14 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Set up git
|
||||
run: |
|
||||
@@ -48,11 +57,9 @@ jobs:
|
||||
git checkout develop
|
||||
git merge -X ours master
|
||||
|
||||
- name: Run post-merge-master script to revert package.json fields
|
||||
run: ./.action-repo/scripts/release/post-merge-master.sh
|
||||
|
||||
- name: Reset dependencies
|
||||
if: inputs.dependencies
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
while IFS= read -r PACKAGE; do
|
||||
[ -z "$PACKAGE" ] && continue
|
||||
@@ -60,26 +67,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..."
|
||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
pnpm 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
|
||||
|
||||
@@ -4,8 +4,6 @@ on:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
NPM_TOKEN:
|
||||
required: false
|
||||
GPG_PASSPHRASE:
|
||||
required: false
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -20,14 +18,6 @@ on:
|
||||
description: Publish to npm
|
||||
type: boolean
|
||||
default: false
|
||||
downstreams:
|
||||
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
|
||||
type: string
|
||||
required: false
|
||||
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
|
||||
@@ -36,22 +26,46 @@ on:
|
||||
description: |
|
||||
The path to the asset you want to upload, if any. You can use glob patterns here.
|
||||
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
|
||||
Relative to `dir`.
|
||||
type: string
|
||||
required: false
|
||||
expected-asset-count:
|
||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||
type: number
|
||||
required: false
|
||||
dist-dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
version-dirs:
|
||||
description: Directories in which to update package.json `version` field
|
||||
type: string
|
||||
required: false
|
||||
outputs:
|
||||
npm-id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
environment: Release
|
||||
needs: checks
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Load GPG key
|
||||
id: gpg
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -59,21 +73,23 @@ jobs:
|
||||
|
||||
- name: Get draft release
|
||||
id: draft-release
|
||||
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
|
||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
draft: true
|
||||
latest: true
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -84,6 +100,7 @@ jobs:
|
||||
|
||||
- name: Prepare variables
|
||||
id: prepare
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
@@ -98,7 +115,7 @@ jobs:
|
||||
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Check version number not in use
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const { VERSION } = process.env;
|
||||
@@ -117,14 +134,17 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: ${{ inputs.dist-dir }}/package.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Handle develop dependencies
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
ret=0
|
||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
||||
@@ -133,13 +153,19 @@ jobs:
|
||||
VERSION=${dep[1]}
|
||||
|
||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
||||
yarn upgrade "$PACKAGE@$VERSION" --exact
|
||||
pnpm add "$PACKAGE@$VERSION" --save-exact
|
||||
git add -u
|
||||
git commit -m "Keep $PACKAGE at $VERSION"
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
- name: Bump package.json versions
|
||||
run: |
|
||||
for DIR in $DIRS; do
|
||||
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
|
||||
git add "$DIR"/package.json
|
||||
done
|
||||
env:
|
||||
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
@@ -161,17 +187,13 @@ jobs:
|
||||
env:
|
||||
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
|
||||
|
||||
- name: Run pre-release script to update package.json fields
|
||||
run: |
|
||||
./.action-repo/scripts/release/pre-release.sh
|
||||
git add package.json
|
||||
|
||||
- name: Commit changes
|
||||
run: git commit -m "$VERSION"
|
||||
|
||||
- name: Build assets
|
||||
if: steps.prepare.outputs.has-dist-script == '1'
|
||||
run: DIST_VERSION="$VERSION" yarn dist
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: DIST_VERSION="$VERSION" pnpm dist
|
||||
|
||||
- name: Upload release assets & signatures
|
||||
if: inputs.asset-path
|
||||
@@ -179,7 +201,7 @@ jobs:
|
||||
with:
|
||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||
asset-path: ${{ inputs.asset-path }}
|
||||
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
|
||||
|
||||
- name: Create signed tag
|
||||
if: inputs.gpg-fingerprint
|
||||
@@ -212,7 +234,7 @@ jobs:
|
||||
|
||||
- name: Validate release has expected assets
|
||||
if: inputs.expected-asset-count
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||
@@ -240,7 +262,7 @@ jobs:
|
||||
git push origin master
|
||||
|
||||
- name: Publish release
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
@@ -272,14 +294,19 @@ jobs:
|
||||
name: Publish to npm
|
||||
needs: release
|
||||
if: inputs.npm
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
dir: ${{ inputs.dist-dir }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
post-release:
|
||||
name: Post release steps
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- id: repository
|
||||
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
@@ -303,29 +330,3 @@ jobs:
|
||||
# wait-interval: 10
|
||||
# check-name: merge
|
||||
# allowed-conclusions: success
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: npm
|
||||
runs-on: ubuntu-latest
|
||||
if: inputs.downstreams
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJSON(inputs.downstreams) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.npm.outputs.id }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
@@ -1,46 +1,53 @@
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
outputs:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
outputs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
persist-credentials: false
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: ${{ inputs.dir }}/package.json
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552 # v3.0.1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
tag: next
|
||||
ignore-scripts: false
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
|
||||
run: npm dist-tag add "$release" latest
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||
echo "id=$release" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.id }}
|
||||
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
|
||||
|
||||
@@ -21,37 +21,81 @@ on:
|
||||
type: boolean
|
||||
default: true
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
secrets: inherit
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
downstreams: '["matrix-org/matrix-react-sdk", "element-hq/element-web"]'
|
||||
|
||||
bump-downstreams:
|
||||
name: Update npm dependency in downstream projects
|
||||
needs: release
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- repo: element-hq/element-web
|
||||
path: apps/web
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
||||
DIR: ${{ matrix.path }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
|
||||
git add "$DIR"/package.json pnpm-lock.yaml
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
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
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: yarn gendoc
|
||||
run: pnpm gendoc
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||
with:
|
||||
path: _docs
|
||||
|
||||
@@ -59,9 +103,14 @@ 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:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||
|
||||
@@ -5,27 +5,33 @@ on:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
# No longer used
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
required: false
|
||||
inputs:
|
||||
sharded:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
|
||||
extra_args:
|
||||
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
|
||||
version-pkg-json-dir:
|
||||
type: string
|
||||
required: false
|
||||
description: "Extra args to pass to SonarCloud"
|
||||
default: "."
|
||||
description: "Relative path of the directory containing package.json with the `version` to use."
|
||||
permissions: {}
|
||||
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'
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -34,56 +40,59 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
persist-credentials: false
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
merge-multiple: true
|
||||
- name: Check coverage artifact
|
||||
run: |
|
||||
if [ ! -d coverage ]; then
|
||||
echo "Coverage not found. Exiting with failure."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
|
||||
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
|
||||
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
|
||||
reports=$(find coverage -type f -name '*sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.7
|
||||
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
skip_checkout: true
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
skip_coverage_label: Z-Skip-Coverage
|
||||
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
extra_args: |
|
||||
${{ inputs.extra_args }}
|
||||
-Dsonar.javascript.lcov.reportPaths=${{ steps.extra_args.outputs.reportPaths }}
|
||||
-Dsonar.testExecutionReportPaths=${{ steps.extra_args.outputs.testExecutionReportPaths }}
|
||||
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to call upon SonarCloud
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -8,85 +8,192 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions: {} # No permissions needed
|
||||
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
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
run: "pnpm run lint:js"
|
||||
|
||||
node_example_lint:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Build Types
|
||||
run: "pnpm build:types"
|
||||
|
||||
- name: Install Example Deps
|
||||
run: "npm install"
|
||||
working-directory: "examples/node"
|
||||
|
||||
- name: Check Syntax
|
||||
run: "node --check app.js"
|
||||
working-directory: "examples/node"
|
||||
|
||||
- name: Typecheck
|
||||
run: "npx tsc"
|
||||
working-directory: "examples/node"
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn lint:workflows"
|
||||
run: "pnpm lint:workflows"
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors"
|
||||
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
# We'll only use this in a workflow_run, then we're done with it
|
||||
retention-days: 1
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run linter
|
||||
run: "pnpm run lint:knip"
|
||||
|
||||
element-web:
|
||||
name: Downstream tsc element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: apps/web
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
# Workflow consolidation job
|
||||
done:
|
||||
needs:
|
||||
- ts_lint
|
||||
- js_lint
|
||||
- node_example_lint
|
||||
- workflow_lint
|
||||
- docs
|
||||
- analyse_dead_code
|
||||
- element-web
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
steps:
|
||||
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Sync labels
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "0 1 * * *" # 1am every day
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
|
||||
with:
|
||||
LABELS: |
|
||||
element-hq/element-meta
|
||||
.github/labels.yml
|
||||
DELETE: true
|
||||
WET: true
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
+61
-25
@@ -10,28 +10,32 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
runs-on: ubuntu-latest
|
||||
test:
|
||||
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
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
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
@@ -39,50 +43,82 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
pnpm test \
|
||||
--coverage=${ENABLE_COVERAGE} \
|
||||
--maxWorkers ${NUM_WORKERS} \
|
||||
./spec/${{ matrix.specs }}
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
SHARD: ${{ matrix.specs }}
|
||||
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
|
||||
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
# Dummy completion job to simplify branch protections
|
||||
complete:
|
||||
name: Tests
|
||||
needs: test
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
element-web:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
statuses: write
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
if: github.event_name == 'merge_group'
|
||||
permissions: read-all # zizmor: ignore[excessive-permissions]
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
use_js_sdk: "."
|
||||
|
||||
# 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-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- complement-crypto
|
||||
steps:
|
||||
- if: needs.complement-crypto.result != 'skipped' && needs.complement-crypto.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
# Hook for branch protection to skip downstream testing outside of merge queues
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
- element-web
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
@@ -91,5 +127,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
|
||||
|
||||
@@ -3,12 +3,12 @@ name: Move new issues into Issue triage board
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||
with:
|
||||
project-url: https://github.com/orgs/element-hq/projects/120
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -3,9 +3,9 @@ name: Move labelled issues to correct projects
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: vector-im/element-web/.github/workflows/triage-labelled.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Close stale PRs
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
permissions: {}
|
||||
jobs:
|
||||
close:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
with:
|
||||
operations-per-run: 250
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 180
|
||||
days-before-pr-close: 0
|
||||
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
|
||||
+1
-2
@@ -13,8 +13,7 @@ out
|
||||
/dist
|
||||
/lib
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
# tarball created by `npm pack` / `yarn pack`
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
|
||||
+873
@@ -1,3 +1,876 @@
|
||||
Changes in [41.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.3.0) (2026-04-07)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Rotate the current room key when we see a member leave ([#5231](https://github.com/matrix-org/matrix-js-sdk/pull/5231)). Contributed by @kaylendog.
|
||||
|
||||
|
||||
Changes in [41.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.2.0) (2026-03-24)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Only share history if room history visibility is shared ([#5216](https://github.com/matrix-org/matrix-js-sdk/pull/5216)). Contributed by @kaylendog.
|
||||
* History sharing: resume key-bundle import on restart ([#5214](https://github.com/matrix-org/matrix-js-sdk/pull/5214)). Contributed by @richvdh.
|
||||
* Move `CryptoApi.shareRoomHistoryWithUser` to `CryptoBackend` ([#5218](https://github.com/matrix-org/matrix-js-sdk/pull/5218)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [41.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.1.0) (2026-03-10)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Throw a specific error when the backup decryption key does not match the public backup ([#5202](https://github.com/matrix-org/matrix-js-sdk/pull/5202)). Contributed by @andybalaam.
|
||||
* Update getUrlPreview to use /\_matrix/client/v1/media/preview\_url ([#5191](https://github.com/matrix-org/matrix-js-sdk/pull/5191)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Download room keys from backup prior to buliding historic room key bundles ([#5171](https://github.com/matrix-org/matrix-js-sdk/pull/5171)). Contributed by @kaylendog.
|
||||
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
|
||||
* Add logging on MSC4108 DELETE request ([#5140](https://github.com/matrix-org/matrix-js-sdk/pull/5140)). Contributed by @reivilibre.
|
||||
* Add `m.invite_permission_config` account data type ([#5183](https://github.com/matrix-org/matrix-js-sdk/pull/5183)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* fix(relations): prevent stale m.replace from overriding newer edits ([#5192](https://github.com/matrix-org/matrix-js-sdk/pull/5192)). Contributed by @basnijholt.
|
||||
* Fix reactive display name disambiguation ([#5135](https://github.com/matrix-org/matrix-js-sdk/pull/5135)). Contributed by @aditya-cherukuru.
|
||||
* Fix empty string to room compatibility trick to only apply to m.call ([#5172](https://github.com/matrix-org/matrix-js-sdk/pull/5172)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [40.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.2.0) (2026-02-10)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* [MatrixRTC] Remove sending of deprecated `notify` event (we now use `m.rtc.notification`) ([#5167](https://github.com/matrix-org/matrix-js-sdk/pull/5167)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Use stable /auth\_metadata endpoint where homeserver supports v1.15 ([#5174](https://github.com/matrix-org/matrix-js-sdk/pull/5174)). Contributed by @hughns.
|
||||
* Support additional\_creators in upgradeRoom (MSC4289) ([#5173](https://github.com/matrix-org/matrix-js-sdk/pull/5173)). Contributed by @andybalaam.
|
||||
* [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId ([#5166](https://github.com/matrix-org/matrix-js-sdk/pull/5166)). Contributed by @toger5.
|
||||
* [MatrixRTC] Do not send the `livekit_alias` in sticky events ([#5165](https://github.com/matrix-org/matrix-js-sdk/pull/5165)). Contributed by @toger5.
|
||||
* Improve startup performance by using `promise.all` when processing rooms from sync ([#5095](https://github.com/matrix-org/matrix-js-sdk/pull/5095)). Contributed by @MidhunSureshR.
|
||||
* Add OAuthGrantType enum for OAuth 2.0 API grant types ([#5161](https://github.com/matrix-org/matrix-js-sdk/pull/5161)). Contributed by @hughns.
|
||||
* Add support for stable OAuth2.0 aware feature from MSC3824 ([#5159](https://github.com/matrix-org/matrix-js-sdk/pull/5159)). Contributed by @hughns.
|
||||
* Give RoomWidgetClient the ability to send and receive sticky events ([#5142](https://github.com/matrix-org/matrix-js-sdk/pull/5142)). Contributed by @robintown.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated ([#5168](https://github.com/matrix-org/matrix-js-sdk/pull/5168)). Contributed by @toger5.
|
||||
* [MatrixRTC] Fix delayId not resetting on leave ([#5156](https://github.com/matrix-org/matrix-js-sdk/pull/5156)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate unused `EventShieldReason` reason codes ([#5127](https://github.com/matrix-org/matrix-js-sdk/pull/5127)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add stable m.oauth UIA stage enum ([#5138](https://github.com/matrix-org/matrix-js-sdk/pull/5138)). Contributed by @hughns.
|
||||
* Add `MatrixEvent.getKeyForwardingUser` ([#5128](https://github.com/matrix-org/matrix-js-sdk/pull/5128)). Contributed by @richvdh.
|
||||
* Add types for (unstable) policy servers ([#5116](https://github.com/matrix-org/matrix-js-sdk/pull/5116)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Recalculate room name on loading members ([#5164](https://github.com/matrix-org/matrix-js-sdk/pull/5164)). Contributed by @RiotRobot.
|
||||
* Avoid rapidly retrying failed requests ([#5146](https://github.com/matrix-org/matrix-js-sdk/pull/5146)). Contributed by @andybalaam.
|
||||
* [matrixRTC] MatrixRTCSessions, add missing event reemission. ([#5144](https://github.com/matrix-org/matrix-js-sdk/pull/5144)). Contributed by @toger5.
|
||||
* Use normal base64 encoding for RTC backend identities ([#5129](https://github.com/matrix-org/matrix-js-sdk/pull/5129)). Contributed by @robintown.
|
||||
* export parseCallNotificationContent and isMyMembership from RTC types ([#5132](https://github.com/matrix-org/matrix-js-sdk/pull/5132)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [40.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.0.0) (2026-01-13)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* MatrixRTC Pseudonymous livekit identities ([#5110](https://github.com/matrix-org/matrix-js-sdk/pull/5110)). Contributed by @toger5.
|
||||
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Mark `forwardingCurve25519KeyChain` as deprecated ([#5111](https://github.com/matrix-org/matrix-js-sdk/pull/5111)). Contributed by @richvdh.
|
||||
* Mark `IEventDecryptionResult` as deprecated ([#5112](https://github.com/matrix-org/matrix-js-sdk/pull/5112)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Implement MSC4387: M\_SAFETY error ([#5107](https://github.com/matrix-org/matrix-js-sdk/pull/5107)). Contributed by @Half-Shot.
|
||||
* Implement \_unstable\_getRTCTransports for MSC4143 ([#5104](https://github.com/matrix-org/matrix-js-sdk/pull/5104)). Contributed by @Half-Shot.
|
||||
* Use `membershipID` for session events ([#5105](https://github.com/matrix-org/matrix-js-sdk/pull/5105)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Make MatrixRTC encryption key types narrower for TS 5.9 compatibility ([#5117](https://github.com/matrix-org/matrix-js-sdk/pull/5117)). Contributed by @robintown.
|
||||
* Re-check outgoing requests after processing them ([#5109](https://github.com/matrix-org/matrix-js-sdk/pull/5109)). Contributed by @andybalaam.
|
||||
* Make token refresher init itself lazily ([#5106](https://github.com/matrix-org/matrix-js-sdk/pull/5106)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [39.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.4.0) (2025-12-16)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Import room key bundles received after invite. ([#5080](https://github.com/matrix-org/matrix-js-sdk/pull/5080)). Contributed by @kaylendog.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Allow msc4354\_sticky\_key to be optional on sticky events. ([#5073](https://github.com/matrix-org/matrix-js-sdk/pull/5073)). Contributed by @Half-Shot.
|
||||
* Handle all response fields from /context API being optional ([#5089](https://github.com/matrix-org/matrix-js-sdk/pull/5089)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.3.0) (2025-12-02)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Re-add truthy check on room name/avatar/alias events ([#5081](https://github.com/matrix-org/matrix-js-sdk/pull/5081)). Contributed by @t3chguy.
|
||||
* Fix invalid state events corrupting room objects ([#5078](https://github.com/matrix-org/matrix-js-sdk/pull/5078)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.2.0) (2025-11-18)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Delayed event management: split endpoints, no auth ([#5066](https://github.com/matrix-org/matrix-js-sdk/pull/5066)). Contributed by @AndrewFerr.
|
||||
* do not set cache in authenticated fetch ([#5020](https://github.com/matrix-org/matrix-js-sdk/pull/5020)). Contributed by @pkuzco.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix media switching during legacy calls ([#5069](https://github.com/matrix-org/matrix-js-sdk/pull/5069)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [39.1.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.2) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
|
||||
|
||||
Changes in [39.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.1) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
Changes in [39.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.0) (2025-11-04)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [MatrixRTC] Sticky Events support (MSC4354) ([#5017](https://github.com/matrix-org/matrix-js-sdk/pull/5017)). Contributed by @toger5.
|
||||
* Add `CryptoApi.getSecretStorageStatus` ([#5054](https://github.com/matrix-org/matrix-js-sdk/pull/5054)). Contributed by @richvdh.
|
||||
* Add parseCallNotificationContent ([#5015](https://github.com/matrix-org/matrix-js-sdk/pull/5015)). Contributed by @toger5.
|
||||
* MSC4140: support filters on delayed event lookup ([#5038](https://github.com/matrix-org/matrix-js-sdk/pull/5038)). Contributed by @AndrewFerr.
|
||||
|
||||
|
||||
Changes in [39.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.0.0) (2025-10-21)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
|
||||
* Implement Sticky Events MSC4354 ([#5028](https://github.com/matrix-org/matrix-js-sdk/pull/5028)). Contributed by @Half-Shot.
|
||||
* feat(client): allow disabling VoIP support ([#5021](https://github.com/matrix-org/matrix-js-sdk/pull/5021)). Contributed by @pkuzco.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Only use the first 3 viaServers specified ([#5034](https://github.com/matrix-org/matrix-js-sdk/pull/5034)). Contributed by @t3chguy.
|
||||
* Fetch the user's device info before processing a verification request ([#5030](https://github.com/matrix-org/matrix-js-sdk/pull/5030)). Contributed by @andybalaam.
|
||||
|
||||
|
||||
Changes in [38.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.4.0) (2025-10-07)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add call intent to RTC call notifications ([#5010](https://github.com/matrix-org/matrix-js-sdk/pull/5010)). Contributed by @Half-Shot.
|
||||
* Implement experimental encrypted state events. ([#4994](https://github.com/matrix-org/matrix-js-sdk/pull/4994)). Contributed by @kaylendog.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Exclude cancelled requests from in-progress lists ([#5016](https://github.com/matrix-org/matrix-js-sdk/pull/5016)). Contributed by @andybalaam.
|
||||
|
||||
|
||||
Changes in [38.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.3.0) (2025-09-23)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Remove knocked room when membership changes to join ([#4977](https://github.com/matrix-org/matrix-js-sdk/pull/4977)). Contributed by @svajunas-budrys.
|
||||
|
||||
|
||||
Changes in [38.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0) (2025-09-16)
|
||||
==================================================================================================
|
||||
Fix [CVE-2025-59160](https://www.cve.org/CVERecord?id=CVE-2025-59160) / [GHSA-mp7c-m3rh-r56v](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56v)
|
||||
|
||||
|
||||
|
||||
Changes in [38.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.1.0) (2025-09-09)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type ([#4979](https://github.com/matrix-org/matrix-js-sdk/pull/4979)). Contributed by @toger5.
|
||||
* MatrixRTC: Add RTC decline event ([#4978](https://github.com/matrix-org/matrix-js-sdk/pull/4978)). Contributed by @toger5.
|
||||
* Make a MatrixRTCSession emit once the RTCNotification is sent ([#4976](https://github.com/matrix-org/matrix-js-sdk/pull/4976)). Contributed by @toger5.
|
||||
* Use hydra semantics for unknown room versions ([#4957](https://github.com/matrix-org/matrix-js-sdk/pull/4957)). Contributed by @dbkr.
|
||||
* Expose the StatusChanged event through the RTCSession ([#4974](https://github.com/matrix-org/matrix-js-sdk/pull/4974)). Contributed by @toger5.
|
||||
* Add `probablyLeft` event to the MatrixRTCSession ([#4962](https://github.com/matrix-org/matrix-js-sdk/pull/4962)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix `m.topic` format ([#4984](https://github.com/matrix-org/matrix-js-sdk/pull/4984)). Contributed by @tulir.
|
||||
* Fix stable-suffixed MSC4133 support ([#4983](https://github.com/matrix-org/matrix-js-sdk/pull/4983)). Contributed by @dbkr.
|
||||
* Fix thread edit aggregation race condition ([#4980](https://github.com/matrix-org/matrix-js-sdk/pull/4980)). Contributed by @basnijholt.
|
||||
|
||||
|
||||
Changes in [38.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.0.0) (2025-08-27)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Release tranche of breaking changes ([#4975](https://github.com/matrix-org/matrix-js-sdk/pull/4975)).
|
||||
* Remove support for FetchHttpApi `onlyData = false`
|
||||
* Remove deprecated `IJoinRoomOpts.syncRoom`
|
||||
* Remove deprecated methods which are unsupported in rust crypto
|
||||
* Remove deprecated getAuthIssuer method
|
||||
* Remove deprecated beginKeyVerification method
|
||||
* Remove deprecated isEncryptedDisabledForUnverifiedDevices getter
|
||||
* Remove deprecated UndecryptableToDeviceEvent MatrixClient emit
|
||||
* Remove deprecated defer utility method
|
||||
* Remove deprecated UIAResponse dummy type
|
||||
* Remove deprecated MatrixRTCSession MembershipConfig fields
|
||||
* Remove deprecated findVerificationRequestDMInProgress and storeSessionBackupPrivateKey methods in favour of overloads
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Allow multiple rtc sessions per room (with different sessionDescriptions) ([#4945](https://github.com/matrix-org/matrix-js-sdk/pull/4945)). Contributed by @toger5.
|
||||
* Add support for login\_hint in authorization url generation ([#4943](https://github.com/matrix-org/matrix-js-sdk/pull/4943)). Contributed by @odelcroi.
|
||||
* Only process MatrixRTC sessions associated with calls for `callMembershipsForRoom` ([#4960](https://github.com/matrix-org/matrix-js-sdk/pull/4960)). Contributed by @fkwp.
|
||||
|
||||
|
||||
Changes in [37.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.13.0) (2025-08-11)
|
||||
====================================================================================================
|
||||
This release supports new v12 Matrix rooms and consequently has a breaking change, removing powerLevelNorm from the RoomMember object as this can't be supported with infinite power levels. Apps should use the non-normalised `powerLevel` instead.
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* [Backport staging] Support v12 rooms in maySendEvent ([#4956](https://github.com/matrix-org/matrix-js-sdk/pull/4956)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
|
||||
* Experimental support for sharing encrypted history on invite ([#4920](https://github.com/matrix-org/matrix-js-sdk/pull/4920)). Contributed by @richvdh.
|
||||
* Use the logger associated with MatrixClient in rust sdk ([#4918](https://github.com/matrix-org/matrix-js-sdk/pull/4918)). Contributed by @richvdh.
|
||||
* Update to matrix-sdk-crypto-wasm 15.1.0, and add new `ShieldStateCode.MismatchedSender` ([#4916](https://github.com/matrix-org/matrix-js-sdk/pull/4916)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix unknown/broken state in the RTC Membership Manager causing unnecassary error logging. ([#4944](https://github.com/matrix-org/matrix-js-sdk/pull/4944)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [37.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.12.0) (2025-07-29)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate non-functional `IJoinRoomOpts.syncRoom` ([#4913](https://github.com/matrix-org/matrix-js-sdk/pull/4913)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Custom abort timeout logic for restarting delayed events that is compatible with the widget api ([#4927](https://github.com/matrix-org/matrix-js-sdk/pull/4927)). Contributed by @toger5.
|
||||
* Allow sending notification events when starting a call ([#4826](https://github.com/matrix-org/matrix-js-sdk/pull/4826)). Contributed by @robintown.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix deep import incompatibility (#4924) ([#4925](https://github.com/matrix-org/matrix-js-sdk/pull/4925)). Contributed by @toriningen.
|
||||
* Fix more incorrect logger use ([#4904](https://github.com/matrix-org/matrix-js-sdk/pull/4904)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [37.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.11.0) (2025-07-15)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Update README to make Element sponsorship explicit ([#4901](https://github.com/matrix-org/matrix-js-sdk/pull/4901)). Contributed by @neilisfragile.
|
||||
* Use client logger in more places (crypto code) ([#4900](https://github.com/matrix-org/matrix-js-sdk/pull/4900)). Contributed by @richvdh.
|
||||
* Use client logger in `MatrixRTCSessionManager` ([#4898](https://github.com/matrix-org/matrix-js-sdk/pull/4898)). Contributed by @richvdh.
|
||||
* Use client logger in more places (core code) ([#4899](https://github.com/matrix-org/matrix-js-sdk/pull/4899)). Contributed by @richvdh.
|
||||
* crypto: Add new `ClientEvent.ReceivedToDeviceMessage` with proper `OlmEncryptionInfo` support ([#4891](https://github.com/matrix-org/matrix-js-sdk/pull/4891)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [37.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.10.0) (2025-07-01)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Update matrix-sdk-crypto-wasm to `15.0.0` ([#4882](https://github.com/matrix-org/matrix-js-sdk/pull/4882)). Contributed by @richvdh.
|
||||
* Allow customizing the IndexedDB database prefix used by Rust crypto. ([#4878](https://github.com/matrix-org/matrix-js-sdk/pull/4878)). Contributed by @clokep.
|
||||
* Remove `@matrix-org/olm` from dependency list ([#4876](https://github.com/matrix-org/matrix-js-sdk/pull/4876)). Contributed by @richvdh.
|
||||
* Redact on ban: Client implementation ([#4867](https://github.com/matrix-org/matrix-js-sdk/pull/4867)). Contributed by @turt2live.
|
||||
|
||||
|
||||
Changes in [37.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.9.0) (2025-06-17)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Ensure we send spec-compliant filter strings by stripping out null values ([#4865](https://github.com/matrix-org/matrix-js-sdk/pull/4865)). Contributed by @t3chguy.
|
||||
* Fix MatrixRTC membership manager failing to rejoin in a race condition (sync vs not found response) ([#4861](https://github.com/matrix-org/matrix-js-sdk/pull/4861)). Contributed by @toger5.
|
||||
* Include extraParams in all HTTP requests ([#4860](https://github.com/matrix-org/matrix-js-sdk/pull/4860)). Contributed by @rsb-tbg.
|
||||
|
||||
|
||||
Changes in [37.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.8.0) (2025-06-10)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Update dependency @matrix-org/matrix-sdk-crypto-wasm to v14.2.1 ([#4869](https://github.com/matrix-org/matrix-js-sdk/pull/4869)). Contributed by @RiotRobot.
|
||||
|
||||
|
||||
Changes in [37.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.7.0) (2025-06-03)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* MatrixRTC: Rename `MembershipConfig` parameters ([#4714](https://github.com/matrix-org/matrix-js-sdk/pull/4714)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Allow the embedded client to work without update\_state support ([#4849](https://github.com/matrix-org/matrix-js-sdk/pull/4849)). Contributed by @robintown.
|
||||
* Check for `unknown variant` on to-device sending and fall back to room event encryption. ([#4847](https://github.com/matrix-org/matrix-js-sdk/pull/4847)). Contributed by @toger5.
|
||||
* Reapply "Distinguish room state and timeline events in embedded clients" ([#4790](https://github.com/matrix-org/matrix-js-sdk/pull/4790)). Contributed by @robintown.
|
||||
|
||||
|
||||
Changes in [37.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.6.0) (2025-05-20)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate utils function `defer` in favour of `Promise.withResolvers` ([#4829](https://github.com/matrix-org/matrix-js-sdk/pull/4829)). Contributed by @t3chguy.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Update to Node 22 LTS ([#4832](https://github.com/matrix-org/matrix-js-sdk/pull/4832)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix autodiscovery handling of 2xx (non-200) codes ([#4833](https://github.com/matrix-org/matrix-js-sdk/pull/4833)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [37.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.5.0) (2025-05-06)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Stabilise MSC3765 ([#4767](https://github.com/matrix-org/matrix-js-sdk/pull/4767)). Contributed by @Johennes.
|
||||
* Inherit `methodFactory` extensions from the parent to the child loggers. ([#4809](https://github.com/matrix-org/matrix-js-sdk/pull/4809)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Fix token refresh behaviour for non-expired tokens ([#4827](https://github.com/matrix-org/matrix-js-sdk/pull/4827)). Contributed by @RiotRobot.
|
||||
* Refactor how token refreshing works to be more resilient ([#4819](https://github.com/matrix-org/matrix-js-sdk/pull/4819)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [37.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.4.0) (2025-04-22)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* MatrixRTC: Add combined `toDeviceAndRoomKeyTransport` ([#4792](https://github.com/matrix-org/matrix-js-sdk/pull/4792)). Contributed by @toger5.
|
||||
* Make logging consistent for matrixRTC ([#4788](https://github.com/matrix-org/matrix-js-sdk/pull/4788)). Contributed by @toger5.
|
||||
* MatrixRTC: ToDevice distribution for media stream keys ([#4785](https://github.com/matrix-org/matrix-js-sdk/pull/4785)). Contributed by @BillCarsonFr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix token refresh racing with other requests and not using new token ([#4798](https://github.com/matrix-org/matrix-js-sdk/pull/4798)). Contributed by @t3chguy.
|
||||
* Fix fallback to MemoryCryptoStore when LocalStorage unavailable ([#4797](https://github.com/matrix-org/matrix-js-sdk/pull/4797)). Contributed by @t3chguy.
|
||||
* Remove duplicate `deleteSecretStorage` in `RustCrypto.resetEncryption` ([#4789](https://github.com/matrix-org/matrix-js-sdk/pull/4789)). Contributed by @florianduros.
|
||||
* Fix `RustCrypto.resetEncryption` failure ([#4772](https://github.com/matrix-org/matrix-js-sdk/pull/4772)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [37.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.3.0) (2025-04-08)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* MatrixRTC MembershipManger: remove redundant sendDelayedEventAction and expose status ([#4747](https://github.com/matrix-org/matrix-js-sdk/pull/4747)). Contributed by @toger5.
|
||||
* Abstract logout-causing error type from tokenRefreshFunction calls ([#4765](https://github.com/matrix-org/matrix-js-sdk/pull/4765)). Contributed by @t3chguy.
|
||||
* Improve PushProcessor::getPushRuleGlobRegex ([#4764](https://github.com/matrix-org/matrix-js-sdk/pull/4764)). Contributed by @t3chguy.
|
||||
* Export push processor \& method for converting matrix glob to regexp ([#4763](https://github.com/matrix-org/matrix-js-sdk/pull/4763)). Contributed by @t3chguy.
|
||||
* Add authenticated media parameter to getMediaConfig ([#4762](https://github.com/matrix-org/matrix-js-sdk/pull/4762)). Contributed by @m004.
|
||||
* Rust crypto: set a timeout on outgoing HTTP requests ([#4761](https://github.com/matrix-org/matrix-js-sdk/pull/4761)). Contributed by @richvdh.
|
||||
* Switch sliding sync support to simplified sliding sync ([#4400](https://github.com/matrix-org/matrix-js-sdk/pull/4400)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [37.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.2.0) (2025-03-25)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add reportRoom API ([#4753](https://github.com/matrix-org/matrix-js-sdk/pull/4753)). Contributed by @Half-Shot.
|
||||
* MatrixRTC: New membership manager ([#4726](https://github.com/matrix-org/matrix-js-sdk/pull/4726)). Contributed by @toger5.
|
||||
* Add disableKeyStorage() to crypto API ([#4742](https://github.com/matrix-org/matrix-js-sdk/pull/4742)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Allow port differing in OIDC dynamic registration URIs ([#4749](https://github.com/matrix-org/matrix-js-sdk/pull/4749)). Contributed by @t3chguy.
|
||||
* OIDC: only pass logo\_uri, policy\_uri, tos\_uri if they conform to "common base" ([#4748](https://github.com/matrix-org/matrix-js-sdk/pull/4748)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [37.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.1.0) (2025-03-11)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* MatrixRTC: MembershipManager test cases and deprecation of MatrixRTCSession.room ([#4713](https://github.com/matrix-org/matrix-js-sdk/pull/4713)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add `EventType.SecretRequest` and `EventType.SecretSend` ([#4728](https://github.com/matrix-org/matrix-js-sdk/pull/4728)). Contributed by @richvdh.
|
||||
* Attest npm package provenance ([#4724](https://github.com/matrix-org/matrix-js-sdk/pull/4724)). Contributed by @t3chguy.
|
||||
* Report backup key import progress on start and improve types ([#4711](https://github.com/matrix-org/matrix-js-sdk/pull/4711)). Contributed by @ajbura.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Handle unexpected token refresh failures gracefully ([#4731](https://github.com/matrix-org/matrix-js-sdk/pull/4731)). Contributed by @t3chguy.
|
||||
* Fix idempotency issue around token refresh ([#4730](https://github.com/matrix-org/matrix-js-sdk/pull/4730)). Contributed by @t3chguy.
|
||||
* Delete the dehydrated device when resetEncryption is called ([#4727](https://github.com/matrix-org/matrix-js-sdk/pull/4727)). Contributed by @uhoreg.
|
||||
|
||||
|
||||
Changes in [37.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.0.0) (2025-02-25)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Remove deprecated `PrefixedLogger` interface ([#4705](https://github.com/matrix-org/matrix-js-sdk/pull/4705)). Contributed by @richvdh.
|
||||
* Remove legacy crypto ([#4653](https://github.com/matrix-org/matrix-js-sdk/pull/4653)). Contributed by @florianduros.
|
||||
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Improve types around User Interactive Auth ([#4709](https://github.com/matrix-org/matrix-js-sdk/pull/4709)). Contributed by @t3chguy.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Fix typos in client.ts ([#4715](https://github.com/matrix-org/matrix-js-sdk/pull/4715)). Contributed by @toger5.
|
||||
* Enable key upload to backups where we have the decryption key ([#4677](https://github.com/matrix-org/matrix-js-sdk/pull/4677)). Contributed by @ajbura.
|
||||
* Remove deprecated `PrefixedLogger` interface ([#4705](https://github.com/matrix-org/matrix-js-sdk/pull/4705)). Contributed by @richvdh.
|
||||
* `MatrixClient.setAccountData`: await remote echo. ([#4695](https://github.com/matrix-org/matrix-js-sdk/pull/4695)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Improve types around User Interactive Auth ([#4709](https://github.com/matrix-org/matrix-js-sdk/pull/4709)). Contributed by @t3chguy.
|
||||
* Fix `resetEncryption` to remove secrets in 4S ([#4683](https://github.com/matrix-org/matrix-js-sdk/pull/4683)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [36.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.2.0) (2025-02-11)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* [Backport staging] Deprecate parameter and functions using legacy crypto in `models/event.ts` ([#4700](https://github.com/matrix-org/matrix-js-sdk/pull/4700)). Contributed by @RiotRobot.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Improve types around Terms ([#4674](https://github.com/matrix-org/matrix-js-sdk/pull/4674)). Contributed by @t3chguy.
|
||||
* Provide more options for starting dehydration ([#4664](https://github.com/matrix-org/matrix-js-sdk/pull/4664)). Contributed by @uhoreg.
|
||||
* Device Dehydration | js-sdk: store/load dehydration key ([#4599](https://github.com/matrix-org/matrix-js-sdk/pull/4599)). Contributed by @BillCarsonFr.
|
||||
* Add unspecced backup disable flag ([#4661](https://github.com/matrix-org/matrix-js-sdk/pull/4661)). Contributed by @dbkr.
|
||||
* Switch OIDC primarily to new `/auth_metadata` API ([#4626](https://github.com/matrix-org/matrix-js-sdk/pull/4626)). Contributed by @t3chguy.
|
||||
* Add `CryptoApi.resetEncryption` ([#4614](https://github.com/matrix-org/matrix-js-sdk/pull/4614)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix topic types ([#4678](https://github.com/matrix-org/matrix-js-sdk/pull/4678)). Contributed by @Half-Shot.
|
||||
* Handle empty m.room.topic ([#4673](https://github.com/matrix-org/matrix-js-sdk/pull/4673)). Contributed by @Half-Shot.
|
||||
* `CryptoApi.resetEncryption` should always create a new key backup ([#4648](https://github.com/matrix-org/matrix-js-sdk/pull/4648)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [36.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.1.0) (2025-01-28)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Deprecate `MatrixClient.login` and replace with `loginRequest` ([#4632](https://github.com/matrix-org/matrix-js-sdk/pull/4632)). Contributed by @richvdh.
|
||||
* Use `SyncCryptoCallback` api instead of legacy crypto in sliding sync ([#4624](https://github.com/matrix-org/matrix-js-sdk/pull/4624)). Contributed by @florianduros.
|
||||
* Distinguish room state and timeline events in embedded clients ([#4574](https://github.com/matrix-org/matrix-js-sdk/pull/4574)). Contributed by @robintown.
|
||||
* Allow setting default secret storage key id to null ([#4615](https://github.com/matrix-org/matrix-js-sdk/pull/4615)). Contributed by @florianduros.
|
||||
* Add authenticated media to getAvatarUrl in room and room-member models ([#4616](https://github.com/matrix-org/matrix-js-sdk/pull/4616)). Contributed by @m004.
|
||||
* Send MSC3981 'recurse' param on `/relations` endpoint on Matrix 1.10 servers ([#4023](https://github.com/matrix-org/matrix-js-sdk/pull/4023)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Revert "Distinguish room state and timeline events in embedded clients (#4574)" ([#4657](https://github.com/matrix-org/matrix-js-sdk/pull/4657)). Contributed by @RiotRobot.
|
||||
* Change randomString et al to be secure ([#4621](https://github.com/matrix-org/matrix-js-sdk/pull/4621)). Contributed by @dbkr.
|
||||
* Fix issue with sentinels being incorrect on m.room.member events ([#4609](https://github.com/matrix-org/matrix-js-sdk/pull/4609)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [36.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.0.0) (2025-01-14)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership ([#4583](https://github.com/matrix-org/matrix-js-sdk/pull/4583)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* MatrixRTC: Implement expiry logic for CallMembership and additional test coverage ([#4587](https://github.com/matrix-org/matrix-js-sdk/pull/4587)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Don't retry on 4xx responses ([#4601](https://github.com/matrix-org/matrix-js-sdk/pull/4601)). Contributed by @dbkr.
|
||||
* Upgrade matrix-sdk-crypto-wasm to 12.1.0 ([#4596](https://github.com/matrix-org/matrix-js-sdk/pull/4596)). Contributed by @andybalaam.
|
||||
* Avoid key prompts when resetting crypto ([#4586](https://github.com/matrix-org/matrix-js-sdk/pull/4586)). Contributed by @dbkr.
|
||||
* Handle when aud OIDC claim is an Array ([#4584](https://github.com/matrix-org/matrix-js-sdk/pull/4584)). Contributed by @liamdiprose.
|
||||
* Save the key backup key to 4S during `bootstrapSecretStorage ` ([#4542](https://github.com/matrix-org/matrix-js-sdk/pull/4542)). Contributed by @dbkr.
|
||||
* Only re-prepare MatrixrRTC delayed disconnection event on 404 ([#4575](https://github.com/matrix-org/matrix-js-sdk/pull/4575)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18)
|
||||
==================================================================================================
|
||||
This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Upgrade matrix-sdk-crypto-wasm to 1.11.0 ([#4593](https://github.com/matrix-org/matrix-js-sdk/pull/4593)).
|
||||
|
||||
|
||||
Changes in [35.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.0.0) (2024-12-17)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
This release contains several breaking changes which will need code changes in your app. Most notably, `initCrypto()`
|
||||
no longer exists and has been moved to `initLegacyCrypto()` in preparation for the eventual removal of Olm. You can
|
||||
continue to use legacy Olm crypto for now by calling `initLegacyCrypto()` instead.
|
||||
|
||||
You may also need to make further changes if you use more advanced APIs. See the individual PRs (listed in order of size of change) for specific APIs changed and how to migrate.
|
||||
|
||||
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
|
||||
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
|
||||
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
|
||||
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate remaining legacy functions and move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler ([#4560](https://github.com/matrix-org/matrix-js-sdk/pull/4560)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
|
||||
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
|
||||
* Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress ([#4561](https://github.com/matrix-org/matrix-js-sdk/pull/4561)). Contributed by @hughns.
|
||||
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
|
||||
* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" ([#4532](https://github.com/matrix-org/matrix-js-sdk/pull/4532)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix age field check in event echo processing ([#3635](https://github.com/matrix-org/matrix-js-sdk/pull/3635)). Contributed by @stas-demydiuk.
|
||||
|
||||
|
||||
Changes in [34.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.13.0) (2024-12-03)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate `MatrixClient.isEventSenderVerified` ([#4527](https://github.com/matrix-org/matrix-js-sdk/pull/4527)). Contributed by @florianduros.
|
||||
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Ensure we disambiguate display names which look like MXIDs ([#4540](https://github.com/matrix-org/matrix-js-sdk/pull/4540)). Contributed by @t3chguy.
|
||||
* Add `CryptoApi.getBackupInfo` ([#4512](https://github.com/matrix-org/matrix-js-sdk/pull/4512)). Contributed by @florianduros.
|
||||
* Fix local echo in embedded mode ([#4498](https://github.com/matrix-org/matrix-js-sdk/pull/4498)). Contributed by @toger5.
|
||||
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix `RustBackupManager` remaining values after current backup removal ([#4537](https://github.com/matrix-org/matrix-js-sdk/pull/4537)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate `MatrixClient.getKeyBackupVersion` ([#4505](https://github.com/matrix-org/matrix-js-sdk/pull/4505)). Contributed by @florianduros.
|
||||
* Deprecate unused callbacks in `CryptoCallbacks` ([#4501](https://github.com/matrix-org/matrix-js-sdk/pull/4501)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Handle M\_MAX\_DELAY\_EXCEEDED errors ([#4511](https://github.com/matrix-org/matrix-js-sdk/pull/4511)). Contributed by @AndrewFerr.
|
||||
* Allow configuration of MatrixRTC timers when calling joinRoomSession() ([#4510](https://github.com/matrix-org/matrix-js-sdk/pull/4510)). Contributed by @hughns.
|
||||
* When state says you've left ongoing call, rejoin ([#4342](https://github.com/matrix-org/matrix-js-sdk/pull/4342)). Contributed by @AndrewFerr.
|
||||
* Remove redundant type arguments in function call ([#4507](https://github.com/matrix-org/matrix-js-sdk/pull/4507)). Contributed by @AndrewFerr.
|
||||
* MatrixRTCSession: handle rate limit errors ([#4494](https://github.com/matrix-org/matrix-js-sdk/pull/4494)). Contributed by @AndrewFerr.
|
||||
* Send/receive error details with widgets ([#4492](https://github.com/matrix-org/matrix-js-sdk/pull/4492)). Contributed by @AndrewFerr.
|
||||
* Capture HTTP error response headers \& handle Retry-After header (MSC4041) ([#4471](https://github.com/matrix-org/matrix-js-sdk/pull/4471)). Contributed by @AndrewFerr.
|
||||
* Add RoomWidgetClient.sendToDeviceViaWidgetApi() ([#4475](https://github.com/matrix-org/matrix-js-sdk/pull/4475)). Contributed by @hughns.
|
||||
|
||||
|
||||
Changes in [34.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.1) (2024-11-12)
|
||||
====================================================================================================
|
||||
# Security
|
||||
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
|
||||
|
||||
Changes in [34.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.0) (2024-11-12)
|
||||
====================================================================================================
|
||||
# Security
|
||||
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
|
||||
|
||||
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
|
||||
|
||||
* Fetch capabilities in the background ([#4246](https://github.com/matrix-org/matrix-js-sdk/pull/4246)). Contributed by @dbkr.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Prefix the user+device state key if needed ([#4262](https://github.com/matrix-org/matrix-js-sdk/pull/4262)). Contributed by @AndrewFerr.
|
||||
* Use legacy call membership if anyone else is ([#4260](https://github.com/matrix-org/matrix-js-sdk/pull/4260)). Contributed by @AndrewFerr.
|
||||
* Fetch capabilities in the background ([#4246](https://github.com/matrix-org/matrix-js-sdk/pull/4246)). Contributed by @dbkr.
|
||||
* Use server name instead of homeserver url to allow well-known lookups during QR OIDC reciprocation ([#4233](https://github.com/matrix-org/matrix-js-sdk/pull/4233)). Contributed by @t3chguy.
|
||||
* Add via parameter for MSC4156 ([#4247](https://github.com/matrix-org/matrix-js-sdk/pull/4247)). Contributed by @Johennes.
|
||||
* Make the js-sdk compatible with MSC preferred foci and active focus. ([#4195](https://github.com/matrix-org/matrix-js-sdk/pull/4195)). Contributed by @toger5.
|
||||
* Replace usages of setImmediate with setTimeout for wider compatibility ([#4240](https://github.com/matrix-org/matrix-js-sdk/pull/4240)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Fix "Unable to restore session" error ([#4299](https://github.com/matrix-org/matrix-js-sdk/pull/4299)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Fix error when sending encrypted messages in large rooms ([#4297](https://github.com/matrix-org/matrix-js-sdk/pull/4297)). Contributed by @RiotRobot.
|
||||
* Element-R: Fix resource leaks in verification logic ([#4263](https://github.com/matrix-org/matrix-js-sdk/pull/4263)). Contributed by @richvdh.
|
||||
* Upgrade Rust Crypto SDK to 6.1.0 ([#4261](https://github.com/matrix-org/matrix-js-sdk/pull/4261)). Contributed by @richvdh.
|
||||
* Correctly transform base64 with multiple instances of + or / ([#4252](https://github.com/matrix-org/matrix-js-sdk/pull/4252)). Contributed by @robintown.
|
||||
* Work around spec bug for m.room.avatar state event content type ([#4245](https://github.com/matrix-org/matrix-js-sdk/pull/4245)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [33.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v33.1.0) (2024-06-18)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* MSC4108 support OIDC QR code login ([#4134](https://github.com/matrix-org/matrix-js-sdk/pull/4134)). Contributed by @t3chguy.
|
||||
* Add crypto methods for export and import of secrets bundle ([#4227](https://github.com/matrix-org/matrix-js-sdk/pull/4227)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix screen sharing in recent Chrome ([#4243](https://github.com/matrix-org/matrix-js-sdk/pull/4243)). Contributed by @RiotRobot.
|
||||
* Fix incorrect assumptions about required fields in /search response ([#4228](https://github.com/matrix-org/matrix-js-sdk/pull/4228)). Contributed by @t3chguy.
|
||||
* Fix the queueToDevice tests for the new fakeindexeddb ([#4225](https://github.com/matrix-org/matrix-js-sdk/pull/4225)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [33.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v33.0.0) (2024-06-04)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Remove more deprecated methods, fields, and exports ([#4217](https://github.com/matrix-org/matrix-js-sdk/pull/4217)). Contributed by @t3chguy.
|
||||
* Remove deprecated methods and fields ([#4201](https://github.com/matrix-org/matrix-js-sdk/pull/4201)). Contributed by @t3chguy.
|
||||
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Remove more deprecated methods, fields, and exports ([#4217](https://github.com/matrix-org/matrix-js-sdk/pull/4217)). Contributed by @t3chguy.
|
||||
* Remove deprecated methods and fields ([#4201](https://github.com/matrix-org/matrix-js-sdk/pull/4201)). Contributed by @t3chguy.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* `initRustCrypto`: allow app to pass in the store key directly ([#4210](https://github.com/matrix-org/matrix-js-sdk/pull/4210)). Contributed by @richvdh.
|
||||
* Preserve ESM for async imports to work correctly ([#4187](https://github.com/matrix-org/matrix-js-sdk/pull/4187)). Contributed by @ms-dosx86.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Don't run migration for Rust crypto if the legacy store is empty ([#4218](https://github.com/matrix-org/matrix-js-sdk/pull/4218)). Contributed by @andybalaam.
|
||||
* Bump matrix-sdk-crypto-wasm to 5.0.0 ([#4216](https://github.com/matrix-org/matrix-js-sdk/pull/4216)). Contributed by @richvdh.
|
||||
* Wire up verification cancel \& mismatch for rust crypto ([#4202](https://github.com/matrix-org/matrix-js-sdk/pull/4202)). Contributed by @t3chguy.
|
||||
* Only pass id\_server if we had one to begin with ([#4200](https://github.com/matrix-org/matrix-js-sdk/pull/4200)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [32.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.4.0) (2024-05-22)
|
||||
==================================================================================================
|
||||
* No changes
|
||||
|
||||
|
||||
Changes in [32.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.3.0) (2024-05-21)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Simplify OIDC types \& export `decodeIdToken` ([#4193](https://github.com/matrix-org/matrix-js-sdk/pull/4193)). Contributed by @t3chguy.
|
||||
* Add helpers for authenticated media, and associated documentation ([#4185](https://github.com/matrix-org/matrix-js-sdk/pull/4185)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix state\_events.ts types ([#4196](https://github.com/matrix-org/matrix-js-sdk/pull/4196)). Contributed by @t3chguy.
|
||||
* Fix sendEventHttpRequest for `m.room.redaction` events without `redacts` ([#4192](https://github.com/matrix-org/matrix-js-sdk/pull/4192)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [32.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.2.0) (2024-05-07)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Use a different error code for UTDs when user was not in the room ([#4172](https://github.com/matrix-org/matrix-js-sdk/pull/4172)). Contributed by @uhoreg.
|
||||
* Modernize window.crypto access constants ([#4169](https://github.com/matrix-org/matrix-js-sdk/pull/4169)). Contributed by @turt2live.
|
||||
* Improve compliance with MSC3266 ([#4155](https://github.com/matrix-org/matrix-js-sdk/pull/4155)). Contributed by @AndrewFerr.
|
||||
* Add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk ([#4152](https://github.com/matrix-org/matrix-js-sdk/pull/4152)). Contributed by @toger5.
|
||||
* Crypto: use a new error code for UTDs from device-relative historical events ([#4139](https://github.com/matrix-org/matrix-js-sdk/pull/4139)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Element-R: Fix rust migration when ssss secret are stored not encryted in cache (old legacy behavior) ([#4168](https://github.com/matrix-org/matrix-js-sdk/pull/4168)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [32.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.1.0) (2024-04-23)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add support for device dehydration v2 (Element R) ([#4062](https://github.com/matrix-org/matrix-js-sdk/pull/4062)). Contributed by @uhoreg.
|
||||
* OIDC improvements in prep of OIDC-QR reciprocation ([#4149](https://github.com/matrix-org/matrix-js-sdk/pull/4149)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Validate backup private key before migrating it ([#4114](https://github.com/matrix-org/matrix-js-sdk/pull/4114)). Contributed by @BillCarsonFr.
|
||||
* ElementR| Retry query backup until it works during migration to avoid spurious correption error popup ([#4113](https://github.com/matrix-org/matrix-js-sdk/pull/4113)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [32.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.0.0) (2024-04-09)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
|
||||
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
|
||||
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add new `decryptExistingEvent` test helper ([#4133](https://github.com/matrix-org/matrix-js-sdk/pull/4133)). Contributed by @richvdh.
|
||||
* Improve types for `sendEvent` ([#4108](https://github.com/matrix-org/matrix-js-sdk/pull/4108)). Contributed by @t3chguy.
|
||||
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
|
||||
* Add new enum for verification methods. ([#4129](https://github.com/matrix-org/matrix-js-sdk/pull/4129)). Contributed by @richvdh.
|
||||
* Add some test utils in a new entrypoint ([#4127](https://github.com/matrix-org/matrix-js-sdk/pull/4127)). Contributed by @richvdh.
|
||||
* Improve types for `sendStateEvent` ([#4105](https://github.com/matrix-org/matrix-js-sdk/pull/4105)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Improve types for `IPowerLevelsContent` and `hasSufficientPowerLevelFor` ([#4128](https://github.com/matrix-org/matrix-js-sdk/pull/4128)). Contributed by @galash13.
|
||||
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
|
||||
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
|
||||
* Extend logic for local notification processing to threads ([#4111](https://github.com/matrix-org/matrix-js-sdk/pull/4111)). Contributed by @dbkr.
|
||||
* Fix public rooms post request search params and body ([#4110](https://github.com/matrix-org/matrix-js-sdk/pull/4110)). Contributed by @ajbura.
|
||||
* Fix bugs with the first reply to a thread ([#4104](https://github.com/matrix-org/matrix-js-sdk/pull/4104)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [31.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.1) (2024-03-28)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
+239
-1
@@ -1,3 +1,241 @@
|
||||
# Contributing code to matrix-js-sdk
|
||||
|
||||
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see
|
||||
[LICENSE](LICENSE)).
|
||||
|
||||
## How to contribute
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
We use GitHub's pull request workflow to review the contribution, and either
|
||||
ask you to make any refinements needed or merge it and make them ourselves.
|
||||
|
||||
Your PR should have a title that describes what change is being made. This
|
||||
is used for the text in the Changelog entry by default (see below), so a good
|
||||
title will tell a user succinctly what change is being made. "Fix bug where
|
||||
cows had five legs" and, "Add support for miniature horses" are examples of good
|
||||
titles. Don't include an issue number here: that belongs in the description.
|
||||
Definitely don't use the GitHub default of "Update file.ts".
|
||||
|
||||
As for your PR description, it should include these things:
|
||||
|
||||
- References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
- Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch. This information is
|
||||
also helpful when we come back to look at this in 6 months and ask "why did
|
||||
we do it like that?" we have a chance of finding out.
|
||||
- Why didn't it work before? Why does it work now? What use cases does it
|
||||
unlock?
|
||||
- If you find yourself adding information on how the code works or why you
|
||||
chose to do it the way you did, make sure this information is instead
|
||||
written as comments in the code itself.
|
||||
- Sometimes a PR can change considerably as it is developed. In this case,
|
||||
the description should be updated to reflect the most recent state of
|
||||
the PR. (It can be helpful to retain the old content under a suitable
|
||||
heading, for additional context.)
|
||||
- Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
- Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
### Changelogs
|
||||
|
||||
There's no need to manually add Changelog entries: we use information in the
|
||||
pull request to populate the information that goes into the changelogs our
|
||||
users see, both for Element Web itself and other projects on which it is based.
|
||||
This is picked up from both labels on the pull request and the `Notes:`
|
||||
annotation in the description. By default, the PR title will be used for the
|
||||
changelog entry, but you can specify more options, as follows.
|
||||
|
||||
To add a longer, more detailed description of the change for the changelog:
|
||||
|
||||
_Fix llama herding bug_
|
||||
|
||||
```
|
||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
||||
```
|
||||
|
||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
||||
the default for PRs labelled with `T-Task`):
|
||||
|
||||
_Remove outdated comment from `Ungulates.ts`_
|
||||
|
||||
```
|
||||
Notes: none
|
||||
```
|
||||
|
||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
||||
an entry in that project's changelog. You can do that too:
|
||||
|
||||
_Fix another herding bug_
|
||||
|
||||
```
|
||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
||||
```
|
||||
|
||||
This example is for Element Web. You can specify:
|
||||
|
||||
- element-web
|
||||
- element-desktop
|
||||
|
||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
||||
to specify in the notes that it's a breaking change - this will be added
|
||||
automatically based on the label - but remember to tell the developer how to
|
||||
migrate:
|
||||
|
||||
_Remove legacy class_
|
||||
|
||||
```
|
||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
||||
```
|
||||
|
||||
Other metadata can be added using labels.
|
||||
|
||||
- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump.
|
||||
- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump.
|
||||
- `T-Defect`: A bug fix (in either code or docs).
|
||||
- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
||||
|
||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
||||
to add them: ask in the PR description or comments.
|
||||
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
## Tests
|
||||
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-js-sdk`, you
|
||||
must include comprehensive unit tests written in Vitest.
|
||||
The existing tests can be found under `spec/unit`
|
||||
|
||||
It's good practice to write tests alongside the code as it ensures the code is testable from
|
||||
the start, and gives you a fast feedback loop while you're developing the
|
||||
functionality. Unit tests are necessary even for bug fixes.
|
||||
|
||||
When writing unit tests, please aim for a high level of test coverage
|
||||
for new code - 80% or greater. If you cannot achieve that, please document
|
||||
why it's not possible in your PR.
|
||||
|
||||
Tests validate that your change works as intended and also document
|
||||
concisely what is being changed. Ideally, your new tests fail
|
||||
prior to your change, and succeed once it has been applied. You may
|
||||
find this simpler to achieve if you write the tests first.
|
||||
|
||||
If you're spiking some code that's experimental and not being used to support
|
||||
production features, exceptions can be made to requirements for tests.
|
||||
Note that tests will still be required in order to ship the feature, and it's
|
||||
strongly encouraged to think about tests early in the process, as adding
|
||||
tests later will become progressively more difficult.
|
||||
|
||||
If you're not sure how to approach writing tests for your change, ask for help
|
||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
||||
|
||||
## Code style
|
||||
|
||||
Code style is documented in [code_style.md](./code_style.md).
|
||||
Contributors are encouraged to it and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and **_never_** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
# Review expectations
|
||||
|
||||
See https://github.com/vector-im/element-meta/wiki/Review-process
|
||||
|
||||
# Merge Strategy
|
||||
|
||||
The preferred method for merging pull requests is squash merging to keep the
|
||||
commit history trim, but it is up to the discretion of the team member merging
|
||||
the change. We do not support rebase merges due to `allchange` being unable to
|
||||
handle them. When merging make sure to leave the default commit title, or
|
||||
at least leave the PR number at the end in brackets like by default.
|
||||
When stacking pull requests, you may wish to do the following:
|
||||
|
||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
||||
|
||||
@@ -11,6 +11,22 @@
|
||||
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
|
||||
browser or in Node.js.
|
||||
|
||||
---
|
||||
|
||||
<picture>
|
||||
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
|
||||
<img src="contrib/element-logo-fallback.png" alt="Element logo">
|
||||
</picture>
|
||||
|
||||
<br>
|
||||
|
||||
Development and maintenance is proudly sponsored by [Element](https://element.io). Element uses the SDK in their flagship [web](https://github.com/element-hq/element-web) and [desktop](https://github.com/element-hq/element-desktop) clients.
|
||||
|
||||
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
|
||||
|
||||
---
|
||||
|
||||
#### Minimum Matrix server version: v1.1
|
||||
|
||||
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
|
||||
@@ -21,20 +37,14 @@ endpoints from before Matrix 1.1, for example.
|
||||
|
||||
# Quickstart
|
||||
|
||||
## In a browser
|
||||
> [!IMPORTANT]
|
||||
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
|
||||
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
|
||||
|
||||
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
|
||||
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
|
||||
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
|
||||
|
||||
## In Node.js
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
|
||||
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
`yarn add matrix-js-sdk`
|
||||
`pnpm add matrix-js-sdk`
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
@@ -44,10 +54,8 @@ client.publicRooms(function (err, data) {
|
||||
});
|
||||
```
|
||||
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
|
||||
See [below](#end-to-end-encryption-support) for how to enable end-to-end-encryption, or check
|
||||
[the Node.js terminal app](https://github.com/matrix-org/matrix-js-sdk/tree/develop/examples/node) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
@@ -101,46 +109,74 @@ Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
});
|
||||
```
|
||||
|
||||
## Authenticated media
|
||||
|
||||
Servers supporting [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) (Matrix 1.11) will require clients, like
|
||||
yours, to include an `Authorization` header when `/download`ing or `/thumbnail`ing media. For NodeJS environments this
|
||||
may be as easy as the following code snippet, though web browsers may need to use [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
|
||||
to append the header when using the endpoints in `<img />` elements and similar.
|
||||
|
||||
```javascript
|
||||
const downloadUrl = client.mxcUrlToHttp(
|
||||
/*mxcUrl=*/ "mxc://example.org/abc123", // the MXC URI to download/thumbnail, typically from an event or profile
|
||||
/*width=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*height=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*resizeMethod=*/ undefined, // part of the thumbnail API. Use as required.
|
||||
/*allowDirectLinks=*/ false, // should generally be left `false`.
|
||||
/*allowRedirects=*/ true, // implied supported with authentication
|
||||
/*useAuthentication=*/ true, // the flag we're after in this example
|
||||
);
|
||||
const img = await fetch(downloadUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${client.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
// Do something with `img`.
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> In future the js-sdk will _only_ return authentication-required URLs, mandating population of the `Authorization` header.
|
||||
|
||||
## What does this SDK do?
|
||||
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
|
||||
|
||||
- Handles syncing (via `/initialSync` and `/events`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
Later versions of the SDK will:
|
||||
|
||||
- Expose a `RoomSummary` which would be suitable for a recents page.
|
||||
- Provide different pluggable storage layers (e.g. local storage, database-backed)
|
||||
- Handles syncing (via `/sync`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
# Usage
|
||||
|
||||
## Conventions
|
||||
## Supported platforms
|
||||
|
||||
### Emitted events
|
||||
`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed),
|
||||
or in browser applications, via a bundler such as Webpack or Vite.
|
||||
|
||||
The SDK will emit events using an `EventEmitter`. It also
|
||||
emits object models (e.g. `Rooms`, `RoomMembers`) when they
|
||||
are updated.
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officially supported.
|
||||
|
||||
## Emitted events
|
||||
|
||||
The SDK raises notifications to the application using
|
||||
[`EventEmitter`s](https://nodejs.org/api/events.html#class-eventemitter). The `MatrixClient` itself
|
||||
implements `EventEmitter`, as do many of the high-level abstractions such as `Room` and `RoomMember`.
|
||||
|
||||
```javascript
|
||||
// Listen for low-level MatrixEvents
|
||||
@@ -161,45 +197,22 @@ client.on(RoomMemberEvent.Typing, function (event, member) {
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
### Promises and Callbacks
|
||||
## Entry points
|
||||
|
||||
Most of the methods in the SDK are asynchronous: they do not directly return a
|
||||
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
|
||||
which will be fulfilled in the future.
|
||||
As well as the primary entry point (`matrix-js-sdk`), there are several other entry points which may be useful:
|
||||
|
||||
The typical usage is something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).then(function(result) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, if you have a Node.js-style `callback(err, result)` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
|
||||
via an optional `callback` argument. The callback support is now deprecated:
|
||||
new methods do not include a `callback` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
|
||||
## Low level types
|
||||
|
||||
There are some low level TypeScript types exported via the `matrix-js-sdk/lib/types` entrypoint to not bloat the main entrypoint.
|
||||
| Entry point | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| `matrix-js-sdk` | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. |
|
||||
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
|
||||
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
|
||||
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
|
||||
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
|
||||
|
||||
## Examples
|
||||
|
||||
This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
core functionality of the SDK. These examples assume the SDK is set up like this:
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
@@ -215,10 +228,10 @@ const matrixClient = sdk.createClient({
|
||||
### Automatically join rooms when invited
|
||||
|
||||
```javascript
|
||||
matrixClient.on(RoomMemberEvent.Membership, function (event, member) {
|
||||
if (member.membership === KnownMembership.Invite && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).then(function () {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
matrixClient.on(RoomEvent.MyMembership, function (room, membership, prevMembership) {
|
||||
if (membership === KnownMembership.Invite) {
|
||||
matrixClient.joinRoom(room.roomId).then(function () {
|
||||
console.log("Auto-joined %s", room.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -297,7 +310,7 @@ This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. Yo
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ pnpm gendoc
|
||||
$ cd docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
@@ -306,41 +319,131 @@ Then visit `http://localhost:8005` to see the API docs.
|
||||
|
||||
# End-to-end encryption support
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the `Olm` global.
|
||||
`matrix-js-sdk`'s end-to-end encryption support is based on the [WebAssembly bindings](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm) of the Rust [matrix-sdk-crypto](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto) library.
|
||||
|
||||
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
|
||||
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
|
||||
initialise the crypto layer.
|
||||
## Initialization
|
||||
|
||||
If the `Olm` global is not available, the SDK will show a warning, as shown
|
||||
below; `initCrypto()` will also fail.
|
||||
To initialize the end-to-end encryption support in the matrix client:
|
||||
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
```javascript
|
||||
// Create a new matrix client
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// Initialize to enable end-to-end encryption support.
|
||||
await matrixClient.initRustCrypto();
|
||||
```
|
||||
|
||||
If the crypto layer is not (successfully) initialised, the SDK will continue to
|
||||
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
|
||||
specification.
|
||||
Note that by default it will attempt to use the Indexed DB provided by the browser as a crypto store. If running outside the browser, you will need to pass [an options object](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#initrustcrypto) which includes `useIndexedDB: false`, to use an ephemeral in-memory store instead. Note that without a persistent store, you'll need to create a new device on the server side (with [`MatrixClient.loginRequest`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#loginrequest)) each time your application starts.
|
||||
|
||||
To provide the Olm library in a browser application:
|
||||
After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto).
|
||||
|
||||
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
|
||||
**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time.
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
## Secret storage
|
||||
|
||||
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://packages.matrix.org/npm/olm/)
|
||||
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
|
||||
You should normally set up [secret storage](https://spec.matrix.org/v1.12/client-server-api/#secret-storage) before using the end-to-end encryption. To do this, call [`CryptoApi.bootstrapSecretStorage`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapSecretStorage).
|
||||
`bootstrapSecretStorage` can be called unconditionally: it will only set up the secret storage if it is not already set up (unless you use the `setupNewSecretStorage` parameter).
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
|
||||
application also works without e2e crypto enabled, add `--optional` to mark it
|
||||
as an optional dependency.
|
||||
```javascript
|
||||
const matrixClient = sdk.createClient({
|
||||
...,
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async (keys) => {
|
||||
// This function should prompt the user to enter their secret storage key.
|
||||
return mySecretStorageKeys;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
matrixClient.getCrypto().bootstrapSecretStorage({
|
||||
// This function will be called if a new secret storage key (aka recovery key) is needed.
|
||||
// You should prompt the user to save the key somewhere, because they will need it to unlock secret storage in future.
|
||||
createSecretStorageKey: async () => {
|
||||
return mySecretStorageKey;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The example above will create a new secret storage key if secret storage was not previously set up.
|
||||
The secret storage data will be encrypted using the secret storage key returned in [`createSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CreateSecretStorageOpts.html#createSecretStorageKey).
|
||||
|
||||
We recommend that you prompt the user to re-enter this key when [`CryptoCallbacks.getSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) is called (when the secret storage access is needed).
|
||||
|
||||
## Set up cross-signing
|
||||
|
||||
To set up cross-signing to verify devices and other users, call
|
||||
[`CryptoApi.bootstrapCrossSigning`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapCrossSigning):
|
||||
|
||||
```javascript
|
||||
matrixClient.getCrypto().bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
return makeRequest(authDict);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The [`authUploadDeviceSigningKeys`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.BootstrapCrossSigningOpts.html#authUploadDeviceSigningKeys)
|
||||
callback is required in order to upload newly-generated public cross-signing keys to the server.
|
||||
|
||||
## Key backup
|
||||
|
||||
If the user doesn't already have a [key backup](https://spec.matrix.org/v1.12/client-server-api/#server-side-key-backups) you should create one:
|
||||
|
||||
```javascript
|
||||
// Check if we have a key backup.
|
||||
// If checkKeyBackupAndEnable returns null, there is no key backup.
|
||||
const hasKeyBackup = (await matrixClient.getCrypto().checkKeyBackupAndEnable()) !== null;
|
||||
|
||||
// Create the key backup
|
||||
await matrixClient.getCrypto().resetKeyBackup();
|
||||
```
|
||||
|
||||
## Verify a new device
|
||||
|
||||
Once the cross-signing is set up on one of your devices, you can verify another device with two methods:
|
||||
|
||||
1. Use `CryptoApi.bootstrapCrossSigning`.
|
||||
|
||||
`bootstrapCrossSigning` will call the [CryptoCallbacks.getSecretStorageKey](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) callback. The device is verified with the private cross-signing keys fetched from the secret storage.
|
||||
|
||||
2. Request an interactive verification against existing devices, by calling [CryptoApi.requestOwnUserVerification](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#requestOwnUserVerification).
|
||||
|
||||
## Migrating from the legacy crypto stack to Rust crypto
|
||||
|
||||
If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initLegacyCrypto()`), you will
|
||||
need to migrate existing devices to the Rust crypto stack.
|
||||
|
||||
This migration happens automatically when you call `initRustCrypto()` instead of `initLegacyCrypto()`,
|
||||
but you need to provide the legacy [`cryptoStore`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#cryptoStore) and [`pickleKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#pickleKey) to [`createClient`](https://matrix-org.github.io/matrix-js-sdk/functions/matrix.createClient.html):
|
||||
|
||||
```javascript
|
||||
// You should provide the legacy crypto store and the pickle key to the matrix client in order to migrate the data.
|
||||
const matrixClient = sdk.createClient({
|
||||
cryptoStore: myCryptoStore,
|
||||
pickleKey: myPickleKey,
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
// The migration will be done automatically when you call `initRustCrypto`.
|
||||
await matrixClient.initRustCrypto();
|
||||
```
|
||||
|
||||
To follow the migration progress, you can listen to the [`CryptoEvent.LegacyCryptoStoreMigrationProgress`](https://matrix-org.github.io/matrix-js-sdk/enums/crypto_api.CryptoEvent.html#LegacyCryptoStoreMigrationProgress) event:
|
||||
|
||||
```javascript
|
||||
// When progress === total === -1, the migration is finished.
|
||||
matrixClient.on(CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress, total) => {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
The Rust crypto stack is not supported in a lot of deprecated methods of [`MatrixClient`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html). If you use them, you should migrate to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html). Also, the legacy `MatrixClient.crypto` object is not available any more: you should use `MatrixClient.getCrypto()` instead.
|
||||
|
||||
# Contributing
|
||||
|
||||
@@ -350,7 +453,7 @@ want to use this SDK, skip this section._
|
||||
First, you need to pull in the right build tools:
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -358,17 +461,17 @@ First, you need to pull in the right build tools:
|
||||
To build a browser version from scratch when developing:
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
$ pnpm build
|
||||
```
|
||||
|
||||
To run tests (Jest):
|
||||
To run tests:
|
||||
|
||||
```
|
||||
$ yarn test
|
||||
$ pnpm test
|
||||
```
|
||||
|
||||
To run linting:
|
||||
|
||||
```
|
||||
$ yarn lint
|
||||
$ pnpm lint
|
||||
```
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/preset-typescript",
|
||||
{
|
||||
rewriteImportExtensions: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
["@babel/plugin-proposal-decorators", { version: "2023-11" }],
|
||||
"@babel/plugin-transform-numeric-separator",
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-object-rest-spread",
|
||||
"@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",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
## Guiding principles
|
||||
|
||||
1. We want the lint rules to feel natural for most team members. No one should have to think too much
|
||||
about the linter.
|
||||
2. We want to stay relatively close to [industry standards](https://google.github.io/styleguide/tsguide.html)
|
||||
to make onboarding easier.
|
||||
3. We describe what good code looks like rather than point out bad examples. We do this to avoid
|
||||
excessively punishing people for writing code which fails the linter.
|
||||
4. When something isn't covered by the style guide, we come up with a reasonable rule rather than
|
||||
claim that it "passes the linter". We update the style guide and linter accordingly.
|
||||
5. While we aim to improve readability, understanding, and other aspects of the code, we deliberately
|
||||
do not let solely our personal preferences drive decisions.
|
||||
6. We aim to have an understandable guide.
|
||||
|
||||
## Coding practices
|
||||
|
||||
1. Lint rules enforce decisions made by this guide. The lint rules and this guide are kept in
|
||||
perfect sync.
|
||||
2. Commit messages are descriptive for the changes. When the project supports squash merging,
|
||||
only the squashed commit needs to have a descriptive message.
|
||||
3. When there is disagreement with a code style approved by the linter, a PR is opened against
|
||||
the lint rules rather than making exceptions on the responsible code PR.
|
||||
4. Rules which are intentionally broken (via eslint-ignore, @ts-ignore, etc) have a comment
|
||||
included in the immediate vicinity for why. Determination of whether this is valid applies at
|
||||
code review time.
|
||||
5. When editing a file, nearby code is updated to meet the modern standards. "Nearby" is subjective,
|
||||
but should be whatever is reasonable at review time. Such an example might be to update the
|
||||
class's code style, but not the file's.
|
||||
1. These changes should be minor enough to include in the same commit without affecting a code
|
||||
reviewer's job.
|
||||
|
||||
## All code
|
||||
|
||||
Unless otherwise specified, the following applies to all code:
|
||||
|
||||
1. Files must be formatted with Prettier.
|
||||
2. 120 character limit per line. Match existing code in the file if it is using a lower guide.
|
||||
3. A tab/indentation is 4 spaces.
|
||||
4. Newlines are Unix.
|
||||
5. A file has a single empty line at the end.
|
||||
6. Lines are trimmed of all excess whitespace, including blank lines.
|
||||
7. Long lines are broken up for readability.
|
||||
|
||||
## TypeScript / JavaScript
|
||||
|
||||
1. Write TypeScript. Turn JavaScript into TypeScript when working in the area.
|
||||
2. Use [TSDoc](https://tsdoc.org/) to document your code. See [Comments](#comments) below.
|
||||
3. Use named exports.
|
||||
4. Use semicolons for block/line termination.
|
||||
1. Except when defining interfaces, classes, and non-arrow functions specifically.
|
||||
5. When a statement's body is a single line, it must be written without curly braces, so long as the body is placed on
|
||||
the same line as the statement.
|
||||
|
||||
```typescript
|
||||
if (x) doThing();
|
||||
```
|
||||
|
||||
6. Blocks for `if`, `for`, `switch` and so on must have a space surrounding the condition, but not
|
||||
within the condition.
|
||||
|
||||
```typescript
|
||||
if (x) {
|
||||
doThing();
|
||||
}
|
||||
```
|
||||
|
||||
7. lowerCamelCase is used for function and variable naming.
|
||||
8. UpperCamelCase is used for general naming.
|
||||
9. Interface names should not be marked with an uppercase `I`.
|
||||
10. One variable declaration per line.
|
||||
11. If a variable is not receiving a value on declaration, its type must be defined.
|
||||
|
||||
```typescript
|
||||
let errorMessage: string;
|
||||
```
|
||||
|
||||
12. Objects can use shorthand declarations, including mixing of types.
|
||||
|
||||
```typescript
|
||||
{
|
||||
room,
|
||||
prop: this.prop,
|
||||
}
|
||||
// ... or ...
|
||||
{ room, prop: this.prop }
|
||||
```
|
||||
|
||||
13. Object keys should always be non-strings when possible.
|
||||
|
||||
```typescript
|
||||
{
|
||||
property: "value",
|
||||
"m.unavoidable": true,
|
||||
[EventType.RoomMessage]: true,
|
||||
}
|
||||
```
|
||||
|
||||
14. If a variable's type should be boolean, make sure it really is one.
|
||||
|
||||
```typescript
|
||||
const isRealUser = !!userId && ...; // good
|
||||
const isRealUser = Boolean(userId) && Boolean(userName); // also good
|
||||
const isRealUser = Boolean(userId) && isReal; // also good (where isReal is another boolean variable)
|
||||
const isRealUser = Boolean(userId && userName); // also fine
|
||||
const isRealUser = Boolean(userId || userName); // good: same as &&
|
||||
const isRealUser = userId && ...; // bad: isRealUser is userId's type, not a boolean
|
||||
|
||||
if (userId) // fine: userId is evaluated for truthiness, not stored as a boolean
|
||||
```
|
||||
|
||||
15. Use `switch` statements when checking against more than a few enum-like values.
|
||||
16. Use `const` for constants, `let` for mutability.
|
||||
17. Describe types exhaustively (ensure noImplictAny would pass).
|
||||
1. Notable exceptions are arrow functions used as parameters, when a void return type is
|
||||
obvious, and when declaring and assigning a variable in the same line.
|
||||
18. Declare member visibility (public/private/protected).
|
||||
19. Private members are private and not prefixed unless required for naming conflicts.
|
||||
1. Convention is to use an underscore or the word "internal" to denote conflicted member names.
|
||||
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
|
||||
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
|
||||
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
|
||||
1. Note that an explicit type is optional if not expected to be used outside of the function call,
|
||||
unlike in this example:
|
||||
|
||||
```typescript
|
||||
interface MyObject {
|
||||
hasString: boolean;
|
||||
}
|
||||
|
||||
type Options = MyObject | string;
|
||||
|
||||
function doThing(arg: Options) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
22. Variables/properties which are `public static` should also be `readonly` when possible.
|
||||
23. Interface and type properties are terminated with semicolons, not commas.
|
||||
24. Prefer arrow formatting when declaring functions for interfaces/types:
|
||||
|
||||
```typescript
|
||||
interface Test {
|
||||
myCallback: (arg: string) => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
25. Prefer a type definition over an inline type. For example, define an interface.
|
||||
26. Always prefer to add types or declare a type over the use of `any`. Prefer inferred types
|
||||
when they are not `any`.
|
||||
1. When using `any`, a comment explaining why must be present.
|
||||
27. `import` should be used instead of `require`, as `require` does not have types.
|
||||
28. Export only what can be reused.
|
||||
29. Prefer a type like `X | null` instead of truly optional parameters.
|
||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
||||
takes an argument that is more often not required than required. An example where the
|
||||
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||
supply the room ID if it knows it, otherwise deliberately acknowledge that it doesn't
|
||||
have one with `null`.
|
||||
|
||||
```typescript
|
||||
function doThingWithRoom(
|
||||
thing: string,
|
||||
room: string | null, // require the caller to specify
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
30. There should be approximately one interface, class, or enum per file unless the file is named
|
||||
"types.ts", "global.d.ts", or ends with "-types.ts".
|
||||
1. The file name should match the interface, class, or enum name.
|
||||
31. Bulk functions can be declared in a single file, though named as "foo-utils.ts" or "utils/foo.ts".
|
||||
32. Imports are grouped by external module imports first, then by internal imports.
|
||||
33. File ordering is not strict, but should generally follow this sequence:
|
||||
1. Licence header
|
||||
2. Imports
|
||||
3. Constants
|
||||
4. Enums
|
||||
5. Interfaces
|
||||
6. Functions
|
||||
7. Classes
|
||||
1. Public/protected/private static properties
|
||||
2. Public/protected/private properties
|
||||
3. Constructors
|
||||
4. Public/protected/private getters & setters
|
||||
5. Protected and abstract functions
|
||||
6. Public/private functions
|
||||
7. Public/protected/private static functions
|
||||
34. Variable names should be noticeably unique from their types. For example, "str: string" instead
|
||||
of "string: string".
|
||||
35. Use double quotes to enclose strings. You may use single quotes if the string contains double quotes.
|
||||
|
||||
```typescript
|
||||
const example1 = "simple string";
|
||||
const example2 = 'string containing "double quotes"';
|
||||
```
|
||||
|
||||
36. Prefer async-await to promise-chaining
|
||||
|
||||
```typescript
|
||||
async function () {
|
||||
const result = await anotherAsyncFunction();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
37. Avoid functions whose fundamental behaviour varies with different parameter types.
|
||||
Multiple return types are fine, but if the function's behaviour is going to change significantly,
|
||||
have two separate functions. For example, `SDKConfig.get()` with a string param which returns the
|
||||
type according to the param given is ok, but `SDKConfig.get()` with no args returning the whole
|
||||
config object would not be: this should just be a separate function.
|
||||
|
||||
## Tests
|
||||
|
||||
1. Tests must be written in TypeScript.
|
||||
2. Mocks are declared below imports, but above everything else.
|
||||
3. Use the following convention template:
|
||||
|
||||
```typescript
|
||||
// Describe the class, component, or file name.
|
||||
describe("FooComponent", () => {
|
||||
// all test inspecific variables go here
|
||||
|
||||
beforeEach(() => {
|
||||
// exclude if not used.
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// exclude if not used.
|
||||
});
|
||||
|
||||
// Use "it should..." terminology
|
||||
it("should call the correct API", async () => {
|
||||
// test-specific variables go here
|
||||
// function calls/state changes go here
|
||||
// expectations go here
|
||||
});
|
||||
});
|
||||
|
||||
// If the file being tested is a utility class:
|
||||
describe("foo-utils", () => {
|
||||
describe("firstUtilFunction", () => {
|
||||
it("should...", async () => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
|
||||
describe("secondUtilFunction", () => {
|
||||
it("should...", async () => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
1. As a general principle: be liberal with comments. This applies to all files: stylesheets as well as
|
||||
JavaScript/TypeScript.
|
||||
|
||||
Good comments not only help future readers understand and maintain the code; they can also encourage good design
|
||||
by clearly setting out how different parts of the codebase interact where that would otherwise be implicit and
|
||||
subject to interpretation.
|
||||
|
||||
2. Aim to document all types, methods, class properties, functions, etc, with [TSDoc](https://tsdoc.org/) doc comments.
|
||||
This is _especially_ important for public interfaces in `matrix-js-sdk`, but is good practice in general.
|
||||
|
||||
Even very simple interfaces can often benefit from a doc-comment, both as a matter of consistency, and because simple
|
||||
interfaces have a habit of becoming more complex over time.
|
||||
|
||||
3. Inside a function, there is no need to comment every line, but consider:
|
||||
- before a particular multiline section of code within the function, give an overview of what it does,
|
||||
to make it easier for a reader to follow the flow through the function as a whole.
|
||||
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
|
||||
on how this function interacts with other parts of the codebase.
|
||||
|
||||
4. When making changes to existing code, authors are expected to read existing comments and make any necessary changes
|
||||
to ensure they remain accurate.
|
||||
|
||||
5. Reviewers are encouraged to consider whether more comments would be useful, and to ask the author to add them.
|
||||
|
||||
It is natural for an author to feel that the code they have just written is "obvious" and that comments would be
|
||||
redundant, whereas in reality it would take some time for reader unfamiliar with the code to understand it. A
|
||||
reviewer is well-placed to make a more objective judgement.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](../README.md)
|
||||
- [Introduction](../README.md)
|
||||
|
||||
# Deep dive
|
||||
|
||||
- [Storage notes](storage-notes.md)
|
||||
- [Unverified devices](warning-on-unverified-devices.md)
|
||||
- [Storage notes](storage-notes.md)
|
||||
- [Unverified devices](warning-on-unverified-devices.md)
|
||||
|
||||
+27
-27
@@ -20,19 +20,19 @@ blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
- Chrome
|
||||
- Log database may fail to start with AbortError
|
||||
- IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
- When near the quota, QuotaExceededError is used more consistently
|
||||
- Firefox
|
||||
- The first error will be QuotaExceededError
|
||||
- Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
- Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
- Can revive access if you reopen the DB
|
||||
- Chrome
|
||||
- Log database may fail to start with AbortError
|
||||
- IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
- When near the quota, QuotaExceededError is used more consistently
|
||||
- Firefox
|
||||
- The first error will be QuotaExceededError
|
||||
- Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
- Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
- Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
- Chrome, Firefox
|
||||
- IndexedDB for origin deleted
|
||||
- Local Storage remains in place
|
||||
- Chrome, Firefox
|
||||
- IndexedDB for origin deleted
|
||||
- Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
- Chrome
|
||||
- Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
- Firefox
|
||||
- Firefox 67 shows a prompt to grant
|
||||
- Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
- Chrome
|
||||
- Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
- Firefox
|
||||
- Firefox 67 shows a prompt to grant
|
||||
- Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
- Chrome, Firefox
|
||||
- Can run this at any time to request an estimate of space remaining
|
||||
- Firefox
|
||||
- Returns `0` for `usage` if a site is persisted
|
||||
- Chrome, Firefox
|
||||
- Can run this at any time to request an estimate of space remaining
|
||||
- Firefox
|
||||
- Returns `0` for `usage` if a site is persisted
|
||||
|
||||
@@ -17,13 +17,13 @@ Warn when you initial sync if the room has any new undefined devices since you w
|
||||
|
||||
Warn when the user tries to send a message:
|
||||
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
|
||||
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
|
||||
|
||||
+24
-25
@@ -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,
|
||||
@@ -89,20 +94,14 @@ rl.on("line", function (line) {
|
||||
);
|
||||
} else if (line.indexOf("/file ") === 0) {
|
||||
var filename = line.split(" ")[1].trim();
|
||||
var stream = fs.createReadStream(filename);
|
||||
matrixClient
|
||||
.uploadContent({
|
||||
stream: stream,
|
||||
name: filename,
|
||||
})
|
||||
.then(function (url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
body: filename,
|
||||
url: JSON.parse(url).content_uri,
|
||||
};
|
||||
matrixClient.sendMessage(viewingRoom.roomId, content);
|
||||
let buffer = fs.readFileSync("./your_file_name");
|
||||
matrixClient.uploadContent(new Blob([buffer])).then(function (response) {
|
||||
matrixClient.sendMessage(viewingRoom.roomId, {
|
||||
msgtype: MsgType.File,
|
||||
body: filename,
|
||||
url: response.content_uri,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
|
||||
printMessages();
|
||||
@@ -138,7 +137,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 +148,7 @@ matrixClient.on("sync", function (state, prevState, data) {
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on("Room", function () {
|
||||
matrixClient.on(ClientEvent.Room, function () {
|
||||
setRoomList();
|
||||
if (!viewingRoom) {
|
||||
printRoomList();
|
||||
@@ -158,11 +157,11 @@ 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
|
||||
}
|
||||
if (!viewingRoom || viewingRoom.roomId !== room.roomId) {
|
||||
if (!viewingRoom || viewingRoom.roomId !== room?.roomId) {
|
||||
return; // not viewing a room or viewing the wrong room.
|
||||
}
|
||||
printLine(event);
|
||||
@@ -305,7 +304,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 +342,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();
|
||||
@@ -381,7 +380,7 @@ function print(str, formatter) {
|
||||
}
|
||||
console.log.apply(console.log, newArgs);
|
||||
} else {
|
||||
console.log.apply(console.log, arguments);
|
||||
console.log.apply(console.log, [...arguments]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,4 +393,4 @@ function fixWidth(str, len) {
|
||||
return str;
|
||||
}
|
||||
|
||||
matrixClient.startClient(numMessagesToShow); // messages for each room.
|
||||
matrixClient.startClient({ initialSyncLimit: numMessagesToShow });
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cli-color": "^2.0.6",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["app.js"]
|
||||
}
|
||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
yarn lint
|
||||
pnpm lint
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Config } from "jest";
|
||||
import { env } from "process";
|
||||
|
||||
const config: Config = {
|
||||
testEnvironment: "node",
|
||||
testMatch: ["<rootDir>/spec/**/*.spec.{js,ts}"],
|
||||
setupFilesAfterEnv: ["<rootDir>/spec/setupTests.ts"],
|
||||
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
|
||||
coverageReporters: ["text-summary", "lcov"],
|
||||
testResultsProcessor: "@casualbot/jest-sonar-reporter",
|
||||
|
||||
// Always print out a summary if there are any failing tests. Normally
|
||||
// a summary is only printed if there are more than 20 test *suites*.
|
||||
reporters: [["default", { summaryThreshold: 0 }]],
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable the GHA reporter
|
||||
if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
const reporters: Config["reporters"] = [
|
||||
["github-actions", { silent: false }],
|
||||
// as above: always show a summary if there were any failing tests.
|
||||
["summary", { summaryThreshold: 0 }],
|
||||
];
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
reporters.push("<rootDir>/spec/slowReporter.js");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { KnipConfig } from "knip";
|
||||
|
||||
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
|
||||
process.env.GITHUB_ACTIONS = "1";
|
||||
|
||||
export default {
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/types.ts",
|
||||
"src/browser-index.ts",
|
||||
"src/indexeddb-worker.ts",
|
||||
"src/crypto-api/index.ts",
|
||||
"src/testing.ts",
|
||||
"src/matrix.ts",
|
||||
"src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
|
||||
"scripts/**",
|
||||
"spec/**",
|
||||
// XXX: these should be re-exported by one of the supported exports
|
||||
"src/matrixrtc/index.ts",
|
||||
"src/sliding-sync.ts",
|
||||
"src/webrtc/groupCall.ts",
|
||||
"src/webrtc/stats/media/mediaTrackStats.ts",
|
||||
"src/rendezvous/RendezvousChannel.ts",
|
||||
],
|
||||
project: ["**/*.{js,ts}"],
|
||||
ignore: ["examples/**"],
|
||||
ignoreDependencies: [
|
||||
// Required for `action-validator`
|
||||
"@action-validator/*",
|
||||
// Used for git pre-commit hooks
|
||||
"husky",
|
||||
// Used in script which only runs in environment with `@octokit/rest` installed
|
||||
"@octokit/rest",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||
"dist",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
includeEntryExports: false,
|
||||
exclude: ["enumMembers"],
|
||||
} satisfies KnipConfig;
|
||||
+71
-69
@@ -1,42 +1,38 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "31.6.1",
|
||||
"version": "41.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn build:dev",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"prepare": "pnpm build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"build": "pnpm build:compile && pnpm build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile": "babel --delete-dir-on-start -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
|
||||
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
"lint:knip": "knip",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"coverage": "pnpm test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
"url": "git+https://github.com/matrix-org/matrix-js-sdk.git"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"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": [
|
||||
@@ -52,19 +48,18 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.6.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
|
||||
"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",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.6.0",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "9"
|
||||
"p-retry": "8",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
@@ -73,64 +68,71 @@
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "^7.25.9",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-class-properties": "^7.12.1",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@fetch-mock/vitest": "^0.2.18",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/node": "22",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"@vitest/eslint-plugin": "^1.6.6",
|
||||
"@vitest/ui": "^4.0.17",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"domexception": "^4.0.0",
|
||||
"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-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^48.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"eslint-plugin-jsdoc": "^62.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"happy-dom": "^20.1.0",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"lint-staged": "^15.0.2",
|
||||
"knip": "^6.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.2.5",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.25.10",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
"prettier": "3.8.3",
|
||||
"typedoc": "^0.28.1",
|
||||
"typedoc-plugin-coverage": "^4.0.0",
|
||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||
"typedoc-plugin-missing-exports": "^4.0.0",
|
||||
"typescript": "^6.0.0",
|
||||
"vitest": "^4.0.17",
|
||||
"vitest-sonar-reporter": "^3.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "8"
|
||||
}
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"eslint": "8"
|
||||
},
|
||||
"overrides": {
|
||||
"expect": "30.3.0",
|
||||
"flatted@<=3.4.1": "^3.4.2",
|
||||
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
|
||||
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
|
||||
"vite": "8.0.8"
|
||||
}
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||
}
|
||||
|
||||
Generated
+8353
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
nodeLinker: hoisted
|
||||
@@ -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 = "";
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||
for i in main typings browser
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# For the published and dist versions of the package,
|
||||
# we copy the `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if they exist).
|
||||
# This small bit of gymnastics allows us to use the TypeScript source directly for development without
|
||||
# needing to build before linting or testing.
|
||||
|
||||
for i in main typings browser
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
done
|
||||
@@ -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();
|
||||
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
sonar.testExecutionReportPaths=coverage/sonar-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
|
||||
+12
-20
@@ -17,29 +17,29 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "./olm-loader";
|
||||
/* eslint-disable @vitest/no-standalone-expect */
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
|
||||
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
|
||||
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
||||
import { logger } from "../src/logger";
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
import { createClient, IStartClientOpts } from "../src/matrix";
|
||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
|
||||
import { ISyncResponder } from "./test-utils/SyncResponder";
|
||||
import { createClient, type IStartClientOpts } from "../src/matrix";
|
||||
import {
|
||||
type ICreateClientOpts,
|
||||
type IDownloadKeyResult,
|
||||
type MatrixClient,
|
||||
PendingEventOrdering,
|
||||
} from "../src/client";
|
||||
import { type IKeysUploadResponse, type IUploadKeysRequest } from "../src/client";
|
||||
import { type ISyncResponder } from "./test-utils/SyncResponder";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock to intercept the HTTP requests, and
|
||||
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||
*/
|
||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
@@ -55,10 +55,6 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
sessionStoreBackend?: Storage,
|
||||
options?: Partial<ICreateClientOpts>,
|
||||
) {
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
|
||||
}
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
const fullOptions: ICreateClientOpts = {
|
||||
@@ -66,13 +62,9 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
...options,
|
||||
};
|
||||
if (!fullOptions.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
fullOptions.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
}
|
||||
this.client = createClient(fullOptions);
|
||||
|
||||
this.deviceKeys = null;
|
||||
|
||||
@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import debug from "debug";
|
||||
|
||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { type AuthDict, createClient, DebugLogger, type MatrixClient } from "../../../src";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { type CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
||||
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import {
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { CryptoEvent } from "../../../src/crypto-api";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -54,11 +56,7 @@ const TEST_DEVICE_ID = "xzcvb";
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
|
||||
// backend. Once we drop support for legacy crypto, it will go away.
|
||||
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
|
||||
|
||||
describe("cross-signing", () => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
@@ -76,42 +74,45 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
return {
|
||||
getSecretStorageKey: (keys, name) => {
|
||||
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
|
||||
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
});
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:cross-signing`)),
|
||||
});
|
||||
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
/** an object which intercepts `/keys/upload` requests on the test homeserver */
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
await aliceClient.initRustCrypto();
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -134,48 +135,46 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// check the cross-signing keys upload
|
||||
expect(fetchMock.called("upload-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
|
||||
// check that the cross-signing keys have been uploaded
|
||||
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
|
||||
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
|
||||
expect(keysBody).toHaveProperty(["master_key", "signatures", TEST_USER_ID, `ed25519:${TEST_DEVICE_ID}`]);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(["self_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
||||
);
|
||||
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
|
||||
});
|
||||
|
||||
newBackendOnly("get cross signing keys from secret storage and import them", async () => {
|
||||
it("get cross signing keys from secret storage and import them", async () => {
|
||||
// Return public cross signing keys
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
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",
|
||||
@@ -222,9 +221,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
|
||||
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
|
||||
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
|
||||
@@ -237,13 +233,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
|
||||
|
||||
// Expect the signature to be uploaded
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// the device should have a signature with the public self cross signing keys.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
|
||||
);
|
||||
expect(body).toHaveProperty([
|
||||
TEST_USER_ID,
|
||||
TEST_DEVICE_ID,
|
||||
"signatures",
|
||||
TEST_USER_ID,
|
||||
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("can bootstrapCrossSigning twice", async () => {
|
||||
@@ -255,11 +255,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
// a second call should do nothing except GET requests
|
||||
fetchMock.mockClear();
|
||||
await bootstrapCrossSigning(authDict);
|
||||
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
|
||||
expect(calls.length).toEqual(0);
|
||||
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
|
||||
});
|
||||
|
||||
newBackendOnly("will upload existing cross-signing keys to an established secret storage", async () => {
|
||||
it("will upload existing cross-signing keys to an established secret storage", async () => {
|
||||
// This rather obscure codepath covers the case that:
|
||||
// - 4S is set up and working
|
||||
// - our device has private cross-signing keys, but has not published them to 4S
|
||||
@@ -267,8 +266,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
|
||||
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
const accountDataAccumulator = new AccountDataAccumulator();
|
||||
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
|
||||
const authDict = { type: "test" };
|
||||
@@ -282,7 +280,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
});
|
||||
|
||||
// Prepare for the cross-signing keys
|
||||
const p = accountDataAccumulator.interceptSetAccountData(":type(m.cross_signing..*)");
|
||||
const p = accountDataAccumulator.waitForAccountData("m.cross_signing.master");
|
||||
|
||||
await bootstrapCrossSigning(authDict);
|
||||
await p;
|
||||
@@ -343,6 +341,67 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
|
||||
expect(isCrossSigningReady).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false if identity is not trusted, even if the secrets are in 4S", async () => {
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
|
||||
// Complete initial sync, to get the 4S account_data events stored
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// For this test we need to have a well-formed 4S setup.
|
||||
const mockSecretInfo = {
|
||||
encrypted: {
|
||||
// Don't care about the actual values here, just need to be present for validation
|
||||
KeyId: {
|
||||
iv: "IVIVIVIVIVIVIV",
|
||||
ciphertext: "CIPHERTEXTB64",
|
||||
mac: "MACMACMAC",
|
||||
},
|
||||
},
|
||||
};
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.secret_storage.key.KeyId",
|
||||
content: {
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
// iv and mac not relevant for this test
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "KeyId",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.master",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
{
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: mockSecretInfo,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Sanity: ensure that the secrets are in 4S
|
||||
const status = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
expect(status.privateKeysInSecretStorage).toBeTruthy();
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("getCrossSigningKeyId", () => {
|
||||
@@ -354,20 +413,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
*/
|
||||
function awaitCrossSigningKeysUpload() {
|
||||
return new Promise<any>((resolve) => {
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
(url, options) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
fetchMock.modifyRoute("upload-cross-signing-keys", {
|
||||
response: (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return {};
|
||||
},
|
||||
// Override the routes define in `mockSetupCrossSigningRequests`
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -398,9 +450,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
|
||||
describe("crossSignDevice", () => {
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// make sure that there is another device which we can sign
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
@@ -410,17 +459,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Wait for legacy crypto to find the device
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([aliceClient.getSafeUserId()]);
|
||||
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("fails for an unknown device", async () => {
|
||||
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
|
||||
});
|
||||
@@ -433,9 +475,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that a sig for the device was uploaded
|
||||
const calls = fetchMock.calls("upload-sigs");
|
||||
const calls = fetchMock.callHistory.calls("upload-sigs");
|
||||
expect(calls.length).toEqual(1);
|
||||
const body = JSON.parse(calls[0][1]!.body as string);
|
||||
const body = JSON.parse(calls[0].options!.body as string);
|
||||
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
|
||||
expect(deviceSig).toHaveProperty("signatures");
|
||||
});
|
||||
|
||||
+754
-1454
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type CallLog } from "fetch-mock";
|
||||
import debug from "debug";
|
||||
|
||||
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index";
|
||||
import { type RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { type AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { emitPromise, EventCounter } from "../../test-utils/test-utils";
|
||||
|
||||
describe("Device dehydration", () => {
|
||||
it("should rehydrate and dehydrate a device", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async (keys: any, name: string) => {
|
||||
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||
},
|
||||
},
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:dehydration`)),
|
||||
});
|
||||
|
||||
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
||||
|
||||
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
|
||||
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
|
||||
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
|
||||
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
|
||||
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
|
||||
|
||||
// count the number of times the dehydration key gets set
|
||||
let setDehydrationCount = 0;
|
||||
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
|
||||
if (event.getType() === "org.matrix.msc3814") {
|
||||
setDehydrationCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
|
||||
// start dehydration -- we start with no dehydrated device, and we
|
||||
// store the dehydrated device that we create
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
},
|
||||
{ name: "get-dehydrated-device" },
|
||||
);
|
||||
let dehydratedDeviceBody: any;
|
||||
let dehydrationCount = 0;
|
||||
let resolveDehydrationPromise: () => void;
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
(callLog) => {
|
||||
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
|
||||
dehydrationCount++;
|
||||
if (resolveDehydrationPromise) {
|
||||
resolveDehydrationPromise();
|
||||
}
|
||||
return {};
|
||||
},
|
||||
{ name: "put-dehydrated-device" },
|
||||
);
|
||||
await crypto.startDehydration();
|
||||
|
||||
expect(dehydrationCount).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
|
||||
// a week later, we should have created another dehydrated device
|
||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||
resolveDehydrationPromise = resolve;
|
||||
});
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await dehydrationPromise;
|
||||
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationCount).toEqual(2);
|
||||
expect(creationEventCounter.counter).toEqual(2);
|
||||
|
||||
// restart dehydration -- rehydrate the device that we created above,
|
||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||
// a new dehydration key will be set
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
},
|
||||
});
|
||||
const eventsResponse = vi.fn((callLog: CallLog) => {
|
||||
// rehydrating should make two calls to the /events endpoint.
|
||||
// The first time will return a single event, and the second
|
||||
// time will return no events (which will signal to the
|
||||
// rehydration function that it can stop)
|
||||
const body = JSON.parse(callLog.options.body as string);
|
||||
const nextBatch = body.next_batch ?? "0";
|
||||
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
|
||||
return {
|
||||
events,
|
||||
next_batch: nextBatch + "1",
|
||||
};
|
||||
});
|
||||
fetchMock.post(
|
||||
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
|
||||
eventsResponse,
|
||||
);
|
||||
await crypto.startDehydration(true);
|
||||
expect(dehydrationCount).toEqual(3);
|
||||
|
||||
expect(setDehydrationCount).toEqual(2);
|
||||
expect(eventsResponse.mock.calls).toHaveLength(2);
|
||||
|
||||
expect(rehydrationStartedCounter.counter).toEqual(1);
|
||||
expect(rehydrationCompletedCounter.counter).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(3);
|
||||
expect(rehydrationProgressCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||
|
||||
// test that if we get an error when we try to rotate, it emits an event
|
||||
fetchMock.modifyRoute("put-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await rotationErrorEventPromise;
|
||||
|
||||
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
|
||||
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||
await crypto.startDehydration(true);
|
||||
await rehydrationErrorEventPromise;
|
||||
|
||||
matrixClient.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
/** create a new secret storage and cross-signing keys */
|
||||
async function initializeSecretStorage(
|
||||
matrixClient: MatrixClient,
|
||||
userId: string,
|
||||
homeserverUrl: string,
|
||||
): Promise<void> {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
});
|
||||
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
|
||||
const accountData: Map<string, object> = new Map();
|
||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = accountData.get(name);
|
||||
if (value) {
|
||||
return value;
|
||||
} else {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = JSON.parse(callLog.options.body as string);
|
||||
accountData.set(name, value);
|
||||
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
|
||||
return {};
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
const crypto = matrixClient.getCrypto()! as RustCrypto;
|
||||
// we need to process a sync so that the OlmMachine will upload keys
|
||||
await crypto.preprocessToDeviceMessages([]);
|
||||
await crypto.onSyncCompleted({});
|
||||
|
||||
// create initial secret storage
|
||||
async function createSecretStorageKey() {
|
||||
return {
|
||||
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||
privateKey: new Uint8Array(32),
|
||||
};
|
||||
}
|
||||
await matrixClient.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await matrixClient.getCrypto()!.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage: true,
|
||||
setupNewKeyBackup: false,
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,37 +14,37 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { Mocked } from "jest-mock";
|
||||
import { type Mocked } from "vitest";
|
||||
|
||||
import {
|
||||
createClient,
|
||||
CryptoApi,
|
||||
CryptoEvent,
|
||||
ICreateClientOpts,
|
||||
IEvent,
|
||||
IMegolmSessionData,
|
||||
MatrixClient,
|
||||
encodeBase64,
|
||||
type IContent,
|
||||
type ICreateClientOpts,
|
||||
type IEvent,
|
||||
type IMegolmSessionData,
|
||||
type MatrixClient,
|
||||
TypedEventEmitter,
|
||||
} from "../../../src";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
import {
|
||||
advanceTimersUntil,
|
||||
awaitDecryption,
|
||||
CRYPTO_BACKENDS,
|
||||
InitCrypto,
|
||||
syncPromise,
|
||||
} from "../../test-utils/test-utils";
|
||||
import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { defer, IDeferred } from "../../../src/utils";
|
||||
import {
|
||||
decodeRecoveryKey,
|
||||
DecryptionFailureCode,
|
||||
CryptoEvent,
|
||||
type CryptoApi,
|
||||
DecryptionKeyDoesNotMatchError,
|
||||
} from "../../../src/crypto-api";
|
||||
import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||
|
||||
@@ -76,10 +76,11 @@ function mockUploadEmitter(
|
||||
expectedVersion: string,
|
||||
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
|
||||
const emitter = new TypedEventEmitter();
|
||||
fetchMock.removeRoute("mock-upload-emitter");
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
(callLog) => {
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version != expectedVersion) {
|
||||
return {
|
||||
status: 403,
|
||||
@@ -90,7 +91,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
@@ -106,19 +107,12 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
{ name: "mock-upload-emitter" },
|
||||
);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
// const newBackendOnly = backend === "libolm" ? test.skip : test;
|
||||
|
||||
describe("megolm-keys backup", () => {
|
||||
let aliceClient: MatrixClient;
|
||||
/** an object which intercepts `/sync` requests on the test homeserver */
|
||||
let syncResponder: SyncResponder;
|
||||
@@ -129,12 +123,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
mockInitialApiRequests(TEST_HOMESERVER_URL);
|
||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||
@@ -145,15 +137,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
aliceClient?.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
||||
@@ -164,7 +153,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
...opts,
|
||||
});
|
||||
await initCrypto(client);
|
||||
await client.initRustCrypto();
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -192,33 +181,36 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
beforeEach(
|
||||
async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
|
||||
// start after saving the private key
|
||||
await aliceClient.startClient();
|
||||
// start after saving the private key
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
|
||||
// XXX: should we automatically re-check after a device becomes verified?
|
||||
await waitForDeviceList();
|
||||
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
});
|
||||
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
|
||||
// XXX: should we automatically re-check after a device becomes verified?
|
||||
await waitForDeviceList();
|
||||
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
} /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */,
|
||||
10000,
|
||||
);
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == "1") {
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
@@ -239,13 +231,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
|
||||
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
// On the first decryption attempt, decryption fails.
|
||||
await awaitDecryption(event);
|
||||
expect(event.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
|
||||
|
||||
// Eventually, decryption succeeds.
|
||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
|
||||
});
|
||||
|
||||
it("handles error on backup query gracefully", async () => {
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
@@ -257,9 +254,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||
await flushBackupRequest();
|
||||
|
||||
const calls = fetchMock.calls("getKey");
|
||||
const calls = fetchMock.callHistory.calls("getKey");
|
||||
expect(calls.length).toEqual(1);
|
||||
expect(calls[0][0]).toEqual(EXPECTED_URL);
|
||||
expect(calls[0].url).toEqual(EXPECTED_URL);
|
||||
|
||||
await flushBackupRequest();
|
||||
|
||||
@@ -278,11 +275,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// Send Alice a message that she won't be able to decrypt
|
||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||
await flushBackupRequest();
|
||||
const calls = fetchMock.calls("getKey");
|
||||
const calls = fetchMock.callHistory.calls("getKey");
|
||||
expect(calls.length).toEqual(1);
|
||||
expect(calls[0][0]).toEqual(EXPECTED_URL);
|
||||
expect(calls[0].url).toEqual(EXPECTED_URL);
|
||||
|
||||
fetchMock.resetHistory();
|
||||
fetchMock.clearHistory();
|
||||
|
||||
// another message
|
||||
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
|
||||
@@ -292,7 +289,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
};
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse2);
|
||||
await flushBackupRequest();
|
||||
expect(fetchMock.calls("getKey").length).toEqual(0);
|
||||
expect(fetchMock.callHistory.calls("getKey").length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,6 +298,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.get(
|
||||
`path:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`,
|
||||
testData.SIGNED_BACKUP_DATA,
|
||||
);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -325,34 +326,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
let onKeyCached: () => void;
|
||||
const awaitKeyCached = new Promise<void>((resolve) => {
|
||||
onKeyCached = resolve;
|
||||
});
|
||||
|
||||
const result = await advanceTimersUntil(
|
||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
cacheCompleteCallback: () => onKeyCached(),
|
||||
},
|
||||
),
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const result = await advanceTimersUntil(aliceCrypto.restoreKeyBackup());
|
||||
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
|
||||
await awaitKeyCached;
|
||||
|
||||
// The key should be now cached
|
||||
const afterCache = await advanceTimersUntil(
|
||||
aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!),
|
||||
);
|
||||
|
||||
expect(afterCache.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -384,9 +365,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
}
|
||||
|
||||
it("Should import full backup in chunks", async function () {
|
||||
const importMockImpl = jest.fn();
|
||||
const importMockImpl = vi.fn();
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
|
||||
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
|
||||
// We need several rooms with several sessions to test chunking
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
||||
@@ -395,17 +376,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = vi.fn();
|
||||
const result = await aliceCrypto.restoreKeyBackup({
|
||||
progressCallback,
|
||||
});
|
||||
|
||||
expect(result.imported).toStrictEqual(expectedTotal);
|
||||
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
|
||||
expect(importMockImpl).toHaveBeenCalledTimes(5);
|
||||
@@ -438,8 +418,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
it("Should continue to process backup if a chunk import fails and report failures", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = jest
|
||||
const importMockImpl = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// Fail to import first chunk
|
||||
@@ -448,23 +427,22 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// Ok for other chunks
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = vi.fn();
|
||||
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
// A chunk failed to import
|
||||
expect(result.imported).toStrictEqual(200);
|
||||
@@ -486,13 +464,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
it("Should continue if some keys fails to decrypt", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = jest.fn();
|
||||
aliceCrypto.importBackedUpRoomKeys = vi.fn();
|
||||
|
||||
const decryptionFailureCount = 2;
|
||||
|
||||
const mockDecryptor = {
|
||||
// DecryptSessions does not reject on decryption failure, but just skip the key
|
||||
decryptSessions: jest.fn().mockImplementation((sessions) => {
|
||||
decryptSessions: vi.fn().mockImplementation((sessions) => {
|
||||
// simulate fail to decrypt 2 keys out of all
|
||||
const decrypted = [];
|
||||
const keys = Object.keys(sessions);
|
||||
@@ -503,80 +481,85 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
}
|
||||
return decrypted;
|
||||
}),
|
||||
free: jest.fn(),
|
||||
free: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.getBackupDecryptor = jest.fn().mockResolvedValue(mockDecryptor);
|
||||
aliceCrypto.getBackupDecryptor = vi.fn().mockResolvedValue(mockDecryptor);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100]);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const result = await aliceCrypto.restoreKeyBackup();
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
// A chunk failed to import
|
||||
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
|
||||
});
|
||||
|
||||
it("recover specific session from backup", async function () {
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
it("Should get the decryption key from the secret storage and restore the key backup", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
|
||||
const fullBackup = createFullBackup(
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const result = await advanceTimersUntil(
|
||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
ROOM_ID,
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
check!.backupInfo!,
|
||||
),
|
||||
);
|
||||
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
|
||||
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
|
||||
const result = await aliceCrypto.restoreKeyBackup();
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("Fails on bad recovery key", async function () {
|
||||
const fullBackup = {
|
||||
it("Should throw an error if the decryption key does not match the backup", async function () {
|
||||
// Given the stored backup decryption key does not match the public backup info
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64_ALT);
|
||||
|
||||
const fullBackup = createFullBackup(
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
// When we load that key, we throw because the keys don't match
|
||||
await expect(aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage()).rejects.toThrow(
|
||||
DecryptionKeyDoesNotMatchError,
|
||||
);
|
||||
});
|
||||
|
||||
it("Should throw an error if the decryption key is not found in cache", async () => {
|
||||
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
|
||||
});
|
||||
|
||||
function createFullBackup(sessionId: string, data: KeyBackupSession) {
|
||||
return {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
[sessionId]: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
await expect(
|
||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("backupLoop", () => {
|
||||
it("Alice should upload known keys when backup is enabled", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404, { name: "room-keys-version" });
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -613,8 +596,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
fetchMock.modifyRoute("room-keys-version", {
|
||||
response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
@@ -623,7 +606,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
@@ -647,7 +630,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
@@ -672,7 +655,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
name: "room-keys-version",
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
@@ -682,7 +665,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
// wait for all keys to be backed up
|
||||
await remainingZeroPromise;
|
||||
@@ -693,10 +676,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
// Let's simulate that a new backup is available by returning error code on key upload
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
|
||||
|
||||
// If we import a new key the loop will try to upload to old version, it will
|
||||
// fail then check the current version and switch if trusted
|
||||
@@ -739,12 +719,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await disableOldBackup;
|
||||
await enableNewBackup;
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
await newKeyUploadPromise;
|
||||
@@ -759,22 +739,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// on the first key upload attempt, simulate a network failure
|
||||
const failurePromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
});
|
||||
});
|
||||
|
||||
// kick the import loop off and wait for the failed request
|
||||
@@ -783,27 +755,21 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
jest.runAllTimers();
|
||||
vi.advanceTimersByTime(10 * 60 * 1000);
|
||||
await failurePromise;
|
||||
|
||||
// Fix the endpoint to do successful uploads
|
||||
const successPromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
|
||||
@@ -816,7 +782,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
// run the timers, which will make the backup loop redo the request
|
||||
await jest.runAllTimersAsync();
|
||||
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
|
||||
await successPromise;
|
||||
await allKeysUploadedPromise;
|
||||
});
|
||||
@@ -824,7 +790,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
it("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -843,9 +809,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// Serve a backup with no trusted signature
|
||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||
|
||||
const checked = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
||||
@@ -855,9 +819,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(backupStatus).toBeNull();
|
||||
|
||||
// Add a valid signature to the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// check that signalling is working
|
||||
const backupPromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -877,6 +839,38 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
it("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
// At this point there is no backup
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||
|
||||
// Return now the backup
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toMatchObject(testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// Delete the backup and we are expecting the key backup to be disabled
|
||||
const keyBackupStatus = Promise.withResolvers<boolean>();
|
||||
aliceClient.once(CryptoEvent.KeyBackupStatus, (enabled) => keyBackupStatus.resolve(enabled));
|
||||
await aliceCrypto.deleteKeyBackupVersion(testData.SIGNED_BACKUP_DATA.version!);
|
||||
expect(await keyBackupStatus.promise).toBe(false);
|
||||
|
||||
// The backup info should not be available anymore
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||
});
|
||||
|
||||
describe("isKeyBackupTrusted", () => {
|
||||
it("does not trust a backup signed by an untrusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
@@ -952,7 +946,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
it("does not enable a backup signed by an untrusted device", async () => {
|
||||
it("enables a backup not signed by a trusted device, when we have the decryption key", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
// download the device list, to match the trusted-device case
|
||||
await aliceClient.startClient();
|
||||
await waitForDeviceList();
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// Alice does *not* trust the device that signed the backup, but *does* have the decryption key.
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
testData.SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: true });
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
|
||||
it("does not enable a backup signed by an untrusted device when we do not have the decryption key", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
|
||||
@@ -977,7 +993,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
@@ -987,9 +1003,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
unsignedBackup.version = "2";
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
@@ -1004,7 +1018,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
@@ -1014,9 +1028,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
|
||||
@@ -1031,25 +1043,19 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(noResult).toBeNull();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
@@ -1058,10 +1064,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
describe("Backup Changed from other sessions", () => {
|
||||
beforeEach(async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
name: "room-keys-version",
|
||||
});
|
||||
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -1094,9 +1102,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
(callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == "1") {
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
@@ -1110,7 +1118,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
{ name: "room-keys" },
|
||||
);
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
|
||||
@@ -1121,7 +1129,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
|
||||
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
|
||||
|
||||
// =====
|
||||
// Second suppose now that the backup has changed to version 2
|
||||
@@ -1132,7 +1140,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
version: "2",
|
||||
};
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { overwriteRoutes: true });
|
||||
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
|
||||
// suppose the new key is now known
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
@@ -1143,13 +1151,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// A check backup should happen at some point
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const awaitHasQueriedNewBackup: IDeferred<void> = defer<void>();
|
||||
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
fetchMock.modifyRoute("room-keys", {
|
||||
response: (callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == newBackup.version) {
|
||||
awaitHasQueriedNewBackup.resolve();
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
@@ -1165,8 +1172,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the new backup.
|
||||
const newMessage: Partial<IEvent> = {
|
||||
@@ -1202,7 +1208,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
|
||||
@@ -1,693 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* This file consists of a set of integration tests which try to simulate
|
||||
* communication via an Olm-encrypted room between two users, Alice and Bob.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `crypto.spec.js`.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "../../olm-loader";
|
||||
|
||||
import type { Session } from "@matrix-org/olm";
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
const aliUserId = "@ali:localhost";
|
||||
const aliDeviceId = "zxcvb";
|
||||
const aliAccessToken = "aseukfgwef";
|
||||
let bobTestClient: TestClient;
|
||||
const bobUserId = "@bob:localhost";
|
||||
const bobDeviceId = "bvcxz";
|
||||
const bobAccessToken = "fewgfkuesa";
|
||||
let aliMessages: IContent[];
|
||||
let bobMessages: IContent[];
|
||||
|
||||
type OlmPayload = ReturnType<Session["encrypt"]>;
|
||||
|
||||
async function bobUploadsDeviceKeys(): Promise<void> {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that querier will query uploader's keys; then flush the http request.
|
||||
*
|
||||
* @returns resolves once the http request has completed.
|
||||
*/
|
||||
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(uploader.deviceKeys).toBeTruthy();
|
||||
|
||||
const uploaderKeys: Record<string, IDeviceKeys> = {};
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
|
||||
querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result: Record<string, Record<string, IDeviceKeys>> = {};
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
|
||||
const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @returns resolves once the http request has completed.
|
||||
*/
|
||||
async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) {
|
||||
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = "";
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return { one_time_keys: result };
|
||||
});
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
// time, and make sure the claim actually happens rather than ploughing on
|
||||
// confusingly.
|
||||
const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500);
|
||||
expect(r).toEqual(1);
|
||||
}
|
||||
|
||||
async function aliDownloadsKeys(): Promise<void> {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
|
||||
const p1 = async () => {
|
||||
await aliTestClient.client.downloadKeys([bobUserId]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
expect(devices.length).toEqual(1);
|
||||
expect(devices[0].deviceId).toEqual("bvcxz");
|
||||
};
|
||||
const p2 = expectAliQueryKeys;
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
await Promise.all([p1(), p2()]);
|
||||
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
// @ts-ignore - protected
|
||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data!.devices[bobUserId]!;
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
|
||||
expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||
});
|
||||
}
|
||||
|
||||
async function clientEnablesEncryption(client: MatrixClient): Promise<void> {
|
||||
await client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
});
|
||||
expect(client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
}
|
||||
const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client);
|
||||
const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client);
|
||||
|
||||
/**
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsFirstMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @returns which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
async function bobSendsReplyMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys().then(expectBobSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
|
||||
aliMessages.push(content);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @returns which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId];
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
|
||||
}
|
||||
|
||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise<IContent>((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function (_path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
await httpBackend.flush(path, 1);
|
||||
return prom;
|
||||
}
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift()!;
|
||||
return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift()!;
|
||||
return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message);
|
||||
}
|
||||
|
||||
async function recvMessage(
|
||||
httpBackend: TestClient["httpBackend"],
|
||||
client: MatrixClient,
|
||||
sender: string,
|
||||
message: IContent,
|
||||
): Promise<void> {
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function (event: MatrixEvent) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
logger.log(client.credentials.userId + " received event", event);
|
||||
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
resolve(event);
|
||||
};
|
||||
client.on(ClientEvent.Event, onEvent);
|
||||
});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
const preDecryptionEvent = await eventPromise;
|
||||
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||
// it may still be being decrypted
|
||||
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toMatchObject({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an initial sync response to the client (which just includes the member
|
||||
* list for our test room).
|
||||
*
|
||||
* @returns which resolves when the sync has been flushed.
|
||||
*/
|
||||
function firstSync(testClient: TestClient): Promise<void> {
|
||||
// send a sync response including our test room.
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: KnownMembership.Join,
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: KnownMembership.Join,
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return testClient.flushSync();
|
||||
}
|
||||
|
||||
describe("MatrixClient crypto", () => {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
||||
await bobTestClient.client.initCrypto();
|
||||
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
||||
});
|
||||
|
||||
it("Bob uploads device keys", bobUploadsDeviceKeys);
|
||||
|
||||
it("handles failures to upload device keys", async () => {
|
||||
// since device keys are uploaded asynchronously, there's not really much to do here other than fail the
|
||||
// upload.
|
||||
bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh"));
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs device keys", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
await aliDownloadsKeys();
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
// tamper bob's keys
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys!;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", async () => {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "bvcxz",
|
||||
keys: {
|
||||
"ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q",
|
||||
"curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ",
|
||||
},
|
||||
user_id: "@eve:localhost",
|
||||
signatures: {
|
||||
"@eve:localhost": {
|
||||
"ed25519:bvcxz":
|
||||
"CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
const [bobDevices, eveDevices] = await Promise.all([
|
||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||
]);
|
||||
// should get an empty list
|
||||
expect(bobDevices).toEqual([]);
|
||||
expect(eveDevices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", async () => {
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "bad_device",
|
||||
keys: {
|
||||
"ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0",
|
||||
"curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc",
|
||||
},
|
||||
user_id: "@bob:localhost",
|
||||
signatures: {
|
||||
"@bob:localhost": {
|
||||
"ed25519:bad_device":
|
||||
"fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Bob starts his client and uploads device keys and one-time keys", async () => {
|
||||
await bobTestClient.start();
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
|
||||
});
|
||||
|
||||
it("Ali sends a message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
});
|
||||
|
||||
it("Bob receives a message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
const message = aliMessages.shift()!;
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function (event: MatrixEvent) {
|
||||
logger.log(bobUserId + " received event", event);
|
||||
resolve(event);
|
||||
};
|
||||
bobTestClient.client.once(ClientEvent.Event, onEvent);
|
||||
});
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
const preDecryptionEvent = await eventPromise;
|
||||
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||
// it may still be being decrypted
|
||||
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliDownloadsKeys();
|
||||
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||
const p1 = sendMessage(aliTestClient.client);
|
||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend).then(function (sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
await Promise.all([p1, p2]);
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
await aliSendsMessage();
|
||||
await bobRecvMessage();
|
||||
});
|
||||
|
||||
it("Bob replies to the message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
await aliRecvMessage();
|
||||
});
|
||||
|
||||
it("Ali does a key query when encryption is enabled", async () => {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
const syncData = {
|
||||
next_batch: "2",
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encryption",
|
||||
skey: "",
|
||||
content: {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
aliTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await aliTestClient.httpBackend.flush("/sync", 1);
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[bobUserId]: {},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
await aliTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => {
|
||||
// Send a response which causes a key upload
|
||||
const httpBackend = aliTestClient.httpBackend;
|
||||
const syncDataEmpty = {
|
||||
next_batch: "a",
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// enqueue expectations:
|
||||
// * Sync with empty one_time_keys => upload keys
|
||||
|
||||
logger.log(aliTestClient + ": starting");
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
||||
|
||||
await Promise.all([aliTestClient.client.startClient({}), httpBackend.flushAllExpected()]);
|
||||
logger.log(aliTestClient + ": started");
|
||||
httpBackend.when("POST", "/keys/upload").respond(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Checks for outgoing room key requests for a given event's session", async () => {
|
||||
const eventA0 = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const eventA1 = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const eventB = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "othersessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
const nonEncryptedEvent = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {},
|
||||
});
|
||||
|
||||
aliTestClient.client.crypto?.onSyncCompleted({});
|
||||
await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0);
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull();
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull();
|
||||
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -16,12 +16,23 @@ limitations under the License.
|
||||
|
||||
import Olm from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
import { IContent, IDeviceKeys, IDownloadKeyResult, IEvent, Keys, MatrixClient, SigningKeys } from "../../../src";
|
||||
import { IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { ISyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
type IContent,
|
||||
type IDeviceKeys,
|
||||
type IDownloadKeyResult,
|
||||
type IEvent,
|
||||
type Keys,
|
||||
type MatrixClient,
|
||||
type SigningKeys,
|
||||
} from "../../../src";
|
||||
import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { type ISyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { type KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
|
||||
/**
|
||||
* @module
|
||||
@@ -85,15 +96,15 @@ export function bootstrapCrossSigningTestOlmAccount(
|
||||
deviceId: string,
|
||||
keyBackupInfo: KeyBackupInfo[] = [],
|
||||
): Partial<IDownloadKeyResult> {
|
||||
const olmAliceMSK = new global.Olm.PkSigning();
|
||||
const olmAliceMSK = new Olm.PkSigning();
|
||||
const masterPrivkey = olmAliceMSK.generate_seed();
|
||||
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
|
||||
|
||||
const olmAliceUSK = new global.Olm.PkSigning();
|
||||
const olmAliceUSK = new Olm.PkSigning();
|
||||
const userPrivkey = olmAliceUSK.generate_seed();
|
||||
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
|
||||
|
||||
const olmAliceSSK = new global.Olm.PkSigning();
|
||||
const olmAliceSSK = new Olm.PkSigning();
|
||||
const sskPrivkey = olmAliceSSK.generate_seed();
|
||||
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
|
||||
|
||||
@@ -181,7 +192,7 @@ export async function createOlmSession(
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
const session = new Olm.Session();
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
}
|
||||
@@ -294,6 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {},
|
||||
state_key: opts.plaintext.hasOwnProperty("state_key")
|
||||
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -406,3 +420,128 @@ export async function establishOlmSession(
|
||||
await syncPromise(testClient);
|
||||
return p2pSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client shares keys with the given recipient
|
||||
*
|
||||
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
|
||||
* to establish an Olm InboundGroupSession.
|
||||
*
|
||||
* @param recipientUserID - the user id of the expected recipient
|
||||
*
|
||||
* @param recipientOlmAccount - Olm.Account for the recipient
|
||||
*
|
||||
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
|
||||
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
|
||||
*
|
||||
* @returns the established inbound group session
|
||||
*/
|
||||
export async function expectSendRoomKey(
|
||||
recipientUserID: string,
|
||||
recipientOlmAccount: Olm.Account,
|
||||
recipientOlmSession: Olm.Session | null = null,
|
||||
): Promise<Olm.InboundGroupSession> {
|
||||
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
|
||||
|
||||
function onSendRoomKey(content: any): Olm.InboundGroupSession {
|
||||
const m = content.messages[recipientUserID].DEVICE_ID;
|
||||
const ct = m.ciphertext[testRecipientKey];
|
||||
|
||||
if (!recipientOlmSession) {
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
recipientOlmSession = new Olm.Session();
|
||||
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
|
||||
} else {
|
||||
expect(ct.type).toEqual(1); // regular message
|
||||
}
|
||||
|
||||
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
|
||||
expect(decrypted.type).toEqual("m.room_key");
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return inboundGroupSession;
|
||||
}
|
||||
return await new Promise<Olm.InboundGroupSession>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(onSendRoomKey(content));
|
||||
return {};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
* @returns the content of the encrypted event
|
||||
*/
|
||||
export function expectEncryptedSendMessageEvent() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event received on rooms/{roomId}/state/m.room.encrypted/{stateKey} endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
|
||||
* @returns the content of the encrypted event
|
||||
*/
|
||||
function expectEncryptedSendStateEvent() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/state/m.room.encrypted/"), (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client sends an encrypted message event
|
||||
*
|
||||
* Waits for an HTTP request to send an encrypted message in the test room.
|
||||
*
|
||||
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
|
||||
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
|
||||
*
|
||||
* @returns The content of the successfully-decrypted event
|
||||
*/
|
||||
export async function expectSendMegolmMessageEvent(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedMessageContent = await expectEncryptedSendMessageEvent();
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
const inboundGroupSession = await inboundGroupSessionPromise;
|
||||
|
||||
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
|
||||
logger.log("Decrypted received megolm message", r);
|
||||
return JSON.parse(r.plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client sends an encrypted state event
|
||||
*
|
||||
* Waits for an HTTP request to send an encrypted state event in the test room.
|
||||
*
|
||||
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
|
||||
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
|
||||
*
|
||||
* @returns The content of the successfully-decrypted state event
|
||||
*/
|
||||
export async function expectSendMegolmStateEvent(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedStateContent = await expectEncryptedSendStateEvent();
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
const inboundGroupSession = await inboundGroupSessionPromise;
|
||||
|
||||
const r: any = inboundGroupSession.decrypt(encryptedStateContent!.ciphertext);
|
||||
logger.log("Decrypted received megolm state event", r);
|
||||
return JSON.parse(r.plaintext);
|
||||
}
|
||||
|
||||
@@ -16,15 +16,17 @@ limitations under the License.
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { createClient, CryptoEvent, IndexedDBCryptoStore } from "../../../src";
|
||||
import { createClient, IndexedDBCryptoStore } from "../../../src";
|
||||
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
|
||||
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
|
||||
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
|
||||
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
|
||||
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
|
||||
import { CryptoEvent } from "../../../src/crypto-api";
|
||||
|
||||
jest.setTimeout(15000);
|
||||
vi.setConfig({ testTimeout: 15000 });
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -65,18 +67,34 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
|
||||
});
|
||||
|
||||
it("should create the meta db if given a pickleKey", async () => {
|
||||
it("should create the indexed db with a custom prefix", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
pickleKey: "testKey",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto({ cryptoDatabasePrefix: "my-prefix" });
|
||||
|
||||
// should have an indexed db now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(expect.arrayContaining(["my-prefix::matrix-sdk-crypto"]));
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storageKey", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
@@ -85,6 +103,26 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storagePassword", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should ignore a second call", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
@@ -97,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
});
|
||||
|
||||
describe("Libolm Migration", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it("should migrate from libolm", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
|
||||
|
||||
@@ -118,7 +152,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
const progressListener = jest.fn();
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
@@ -162,6 +196,142 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
|
||||
}, 60000);
|
||||
|
||||
describe("Private key backup migration", () => {
|
||||
it("should not migrate the backup private key if backup has changed", async () => {
|
||||
// Here we have a new backup server side, and the migrated account has the previous backup key.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.newBackupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should not migrate the backup private key if backup has unknown algorithm", async () => {
|
||||
// Here we have a new backup server side, and the migrated account has the previous backup key.
|
||||
const backupResponse = {
|
||||
...MSK_NOT_CACHED_DATASET.backupResponse,
|
||||
algorithm: "m.megolm_backup.v8",
|
||||
};
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should not migrate the backup private key if the backup has been deleted", async () => {
|
||||
// The old backup has been deleted server side.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should migrate the backup private key if the backup matches", async () => {
|
||||
// The old backup has been deleted server side.
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
|
||||
|
||||
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: MSK_NOT_CACHED_DATASET.userId,
|
||||
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
|
||||
expect(privateBackupKey).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not migrate if account data is missing", async () => {
|
||||
// See https://github.com/element-hq/element-web/issues/27447
|
||||
|
||||
// Given we have an almost-empty legacy account in the database
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No backup found" },
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/query", EMPTY_ACCOUNT_DATASET.keyQueryResponse);
|
||||
|
||||
const testStoreName = "test-store";
|
||||
await populateStore(testStoreName, EMPTY_ACCOUNT_DATASET.dumpPath);
|
||||
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: EMPTY_ACCOUNT_DATASET.userId,
|
||||
deviceId: EMPTY_ACCOUNT_DATASET.deviceId,
|
||||
cryptoStore,
|
||||
pickleKey: EMPTY_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
// When we start Rust crypto, potentially triggering an upgrade
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// Then no error occurs, and no upgrade happens
|
||||
expect(progressListener.mock.calls.length).toBe(0);
|
||||
}, 60000);
|
||||
|
||||
describe("Legacy trust migration", () => {
|
||||
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
|
||||
const testStoreName = "test-store";
|
||||
@@ -295,10 +465,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();
|
||||
|
||||
@@ -306,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not fail in environments without indexedDB", async () => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = undefined!;
|
||||
@@ -320,4 +490,22 @@ describe("MatrixClient.clearStores", () => {
|
||||
await matrixClient.clearStores();
|
||||
// No error thrown in clearStores
|
||||
});
|
||||
|
||||
it("should clear the indexeddbs with a custom prefix", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ cryptoDatabasePrefix: "my-prefix" });
|
||||
expect(await indexedDB.databases()).toHaveLength(1);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
await matrixClient.clearStores({ cryptoDatabasePrefix: "my-prefix" });
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { TEST_ROOM_ID as ROOM_ID } from "../../test-utils/test-data";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
createClient,
|
||||
HistoryVisibility,
|
||||
PendingEventOrdering,
|
||||
type IStartClientOpts,
|
||||
type MatrixClient,
|
||||
} from "../../../src/matrix";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
createOlmAccount,
|
||||
createOlmSession,
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
getTestOlmAccountKeys,
|
||||
expectSendRoomKey,
|
||||
expectSendMegolmStateEvent,
|
||||
} from "./olm-utils";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
|
||||
describe("Encrypted State Events", () => {
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = "";
|
||||
|
||||
/** the MatrixClient under test */
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
||||
let keyReceiver: E2EKeyReceiver;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
|
||||
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(aliceClient.getUserId() + ": starting");
|
||||
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
|
||||
|
||||
aliceClient.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
...opts,
|
||||
});
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
logger.log(aliceClient.getUserId() + ": started");
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.catch(404);
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: "@alice:localhost",
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
logger: logger.getChild("aliceClient"),
|
||||
enableEncryptedStateEvents: true,
|
||||
});
|
||||
|
||||
keyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
await aliceClient.initRustCrypto();
|
||||
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
testOlmAccount = await createOlmAccount();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
}, 10000);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
function expectAliceKeyQuery(response: any) {
|
||||
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
|
||||
}
|
||||
|
||||
function expectAliceKeyClaim(response: any) {
|
||||
fetchMock.postOnce(new RegExp("/keys/claim"), response);
|
||||
}
|
||||
|
||||
function getTestKeysClaimResponse(userId: string) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
|
||||
const oneTimeKey: string = testOneTimeKeys.curve25519[keyId];
|
||||
const unsignedKeyResult = { key: oneTimeKey };
|
||||
const j = anotherjson.stringify(unsignedKeyResult);
|
||||
const sig = testOlmAccount.sign(j);
|
||||
const keyResult = {
|
||||
...unsignedKeyResult,
|
||||
signatures: { [userId]: { "ed25519:DEVICE_ID": sig } },
|
||||
};
|
||||
|
||||
return {
|
||||
one_time_keys: { [userId]: { DEVICE_ID: { ["signed_curve25519:" + keyId]: keyResult } } },
|
||||
failures: {},
|
||||
};
|
||||
}
|
||||
|
||||
it("Should receive an encrypted state event", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: keyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: keyReceiver.getSigningKey(),
|
||||
olmAccount: testOlmAccount,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a state event with the group session
|
||||
const eventEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
plaintext: {
|
||||
type: "m.room.topic",
|
||||
state_key: "",
|
||||
content: {
|
||||
topic: "Secret!",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [eventEncrypted] } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
|
||||
// it probably won't be decrypted yet, because it takes a while to process the olm keys
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(decryptedEvent.getContent().topic).toEqual("Secret!");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Should send an encrypted state event", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
keyResponder.addDeviceKeys(testDeviceKeys);
|
||||
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and claim one of Bob's OTKs ...
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// ... and send an m.room.topic message
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
||||
await Promise.all([
|
||||
aliceClient.setRoomTopic(ROOM_ID, "Secret!"),
|
||||
expectSendMegolmStateEvent(inboundGroupSessionPromise),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
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/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import {
|
||||
ClientEvent,
|
||||
createClient,
|
||||
type IToDeviceEvent,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
type ReceivedToDeviceMessage,
|
||||
} from "../../../src";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// 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("to-device-messages", () => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
let e2eKeyReceiver: E2EKeyReceiver;
|
||||
let syncResponder: SyncResponder;
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
|
||||
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);
|
||||
e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
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 aliceClient.initRustCrypto();
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
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?
|
||||
});
|
||||
});
|
||||
|
||||
describe("receive to-device-messages", () => {
|
||||
it("Should receive decrypted to-device message via ClientEvent", async () => {
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
await Olm.init();
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
e2eKeyResponder.addDeviceKeys(testDeviceKeys);
|
||||
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const p2pSession = await establishOlmSession(aliceClient, e2eKeyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
const toDeviceEvent = encryptOlmEvent({
|
||||
sender: "@bob:xyz",
|
||||
senderKey: testDeviceKeys.keys[`curve25519:DEVICE_ID`],
|
||||
senderSigningKey: testDeviceKeys.keys[`ed25519:DEVICE_ID`],
|
||||
p2pSession: p2pSession,
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
|
||||
plaincontent: {
|
||||
body: "foo",
|
||||
},
|
||||
plaintype: "m.test.type",
|
||||
});
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
expect(toDeviceEvent.type).toBe("m.room.encrypted");
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
expect(encryptionInfo).not.toBeNull();
|
||||
expect(encryptionInfo!.senderVerified).toBe(false);
|
||||
expect(encryptionInfo!.sender).toBe("@bob:xyz");
|
||||
expect(encryptionInfo!.senderDevice).toBe("DEVICE_ID");
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(true);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
|
||||
it("Should receive clear to-device message via ClientEvent", async () => {
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceEvent: IToDeviceEvent = {
|
||||
sender: "@bob:xyz",
|
||||
type: "m.test.type",
|
||||
content: {
|
||||
body: "foo",
|
||||
},
|
||||
};
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
// When the message is not encrypted, we don't have the encryptionInfo.
|
||||
expect(encryptionInfo).toBeNull();
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(false);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,42 +17,37 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import debug from "debug";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { createHash } from "crypto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import {
|
||||
createClient,
|
||||
CryptoEvent,
|
||||
DebugLogger,
|
||||
DeviceVerification,
|
||||
IContent,
|
||||
ICreateClientOpts,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
type IContent,
|
||||
type ICreateClientOpts,
|
||||
type IEvent,
|
||||
type MatrixClient,
|
||||
MatrixError,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../../src";
|
||||
import {
|
||||
canAcceptVerificationRequest,
|
||||
ShowQrCodeCallbacks,
|
||||
ShowSasCallbacks,
|
||||
type ShowQrCodeCallbacks,
|
||||
type ShowSasCallbacks,
|
||||
VerificationPhase,
|
||||
VerificationRequest,
|
||||
type VerificationRequest,
|
||||
VerificationRequestEvent,
|
||||
Verifier,
|
||||
type Verifier,
|
||||
VerifierEvent,
|
||||
} from "../../../src/crypto-api/verification";
|
||||
import { defer, escapeRegExp } from "../../../src/utils";
|
||||
import {
|
||||
awaitDecryption,
|
||||
CRYPTO_BACKENDS,
|
||||
emitPromise,
|
||||
getSyncResponse,
|
||||
InitCrypto,
|
||||
syncPromise,
|
||||
} from "../../test-utils/test-utils";
|
||||
import { escapeRegExp, sleep } from "../../../src/utils";
|
||||
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise, waitFor } from "../../test-utils/test-utils";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
BACKUP_DECRYPTION_KEY_BASE64,
|
||||
@@ -78,27 +73,28 @@ import {
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
encryptSecretSend,
|
||||
ToDeviceEvent,
|
||||
getTestOlmAccountKeys,
|
||||
type ToDeviceEvent,
|
||||
} from "./olm-utils";
|
||||
import { KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
|
||||
import { encodeBase64 } from "../../../src/base64";
|
||||
|
||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
await global.Olm.init();
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
@@ -115,12 +111,7 @@ const TEST_HOMESERVER_URL = "https://alice-server.com";
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
// we test with both crypto stacks...
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
|
||||
// backend. Once we drop support for legacy crypto, it will go away.
|
||||
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
|
||||
|
||||
describe("verification", () => {
|
||||
/** the client under test */
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
@@ -136,7 +127,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||
@@ -147,14 +137,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
aliceClient?.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
if (vi.isFakeTimers()) {
|
||||
await vi.runAllTimersAsync();
|
||||
}
|
||||
});
|
||||
|
||||
describe("Outgoing verification requests for another device", () => {
|
||||
@@ -162,11 +150,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
// pretend that we have another device, which we will verify
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
|
||||
{ ok: false, status: 404 },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
});
|
||||
|
||||
// test with (1) the default verification method list, (2) a custom verification method list.
|
||||
@@ -218,7 +205,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
if (methods !== undefined) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
|
||||
}
|
||||
|
||||
@@ -251,7 +238,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
vi.advanceTimersByTime(10);
|
||||
|
||||
requestBody = await sendToDevicePromise;
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
@@ -263,7 +250,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new Olm.SAS();
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
@@ -329,7 +316,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// And now Alice starts a SAS verification
|
||||
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
@@ -357,7 +344,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new Olm.SAS();
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||
|
||||
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||
@@ -430,9 +417,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(requests[0].transactionId).toEqual(transactionId);
|
||||
}
|
||||
|
||||
// legacy crypto picks devices individually; rust crypto uses a broadcast message
|
||||
const toDeviceMessage =
|
||||
requestBody.messages[TEST_USER_ID]["*"] ?? requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
// rust crypto uses a broadcast message
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID]["*"];
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
});
|
||||
@@ -472,21 +458,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
|
||||
// we should now have QR data we can display
|
||||
const qrCodeBuffer = (await request.generateQRCode())!;
|
||||
expect(qrCodeBuffer).toBeTruthy();
|
||||
const rawQrCodeBuffer = (await request.generateQRCode())!;
|
||||
expect(rawQrCodeBuffer).toBeTruthy();
|
||||
const qrCodeBuffer = new Uint8Array(rawQrCodeBuffer);
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
|
||||
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
|
||||
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
|
||||
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
|
||||
const txnIdLen = qrCodeBuffer.readUint16BE(8);
|
||||
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
|
||||
expect(textDecoder.decode(qrCodeBuffer.slice(0, 6))).toEqual("MATRIX");
|
||||
expect(qrCodeBuffer[6]).toEqual(0x02); // version
|
||||
expect(qrCodeBuffer[7]).toEqual(0x02); // mode
|
||||
const txnIdLen = (qrCodeBuffer[8] << 8) + qrCodeBuffer[9];
|
||||
expect(textDecoder.decode(qrCodeBuffer.slice(10, 10 + txnIdLen))).toEqual(transactionId);
|
||||
// Alice's device's public key comes next, but we have nothing to do with it here.
|
||||
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
|
||||
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
|
||||
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
|
||||
// const aliceDevicePubKey = qrCodeBuffer.slice(10 + txnIdLen, 32 + 10 + txnIdLen);
|
||||
expect(encodeUnpaddedBase64(qrCodeBuffer.slice(42 + txnIdLen, 32 + 42 + txnIdLen))).toEqual(
|
||||
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||
);
|
||||
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
|
||||
const sharedSecret = qrCodeBuffer.slice(74 + txnIdLen);
|
||||
|
||||
// we should still be "Ready" and have no verifier
|
||||
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||
@@ -518,18 +506,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
reciprocateQRCodeCallbacks.confirm();
|
||||
await sendToDevicePromise;
|
||||
|
||||
// at this point, on legacy crypto, the master key is already marked as trusted, and the request is "Done".
|
||||
// Rust crypto, on the other hand, waits for the 'done' to arrive from the other side.
|
||||
// Rust crypto waits for the 'done' to arrive from the other side.
|
||||
if (request.phase === VerificationPhase.Done) {
|
||||
// legacy crypto: we're all done
|
||||
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
||||
await verificationPromise;
|
||||
} else {
|
||||
// rust crypto: still in flight
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(request.phase).toEqual(VerificationPhase.Started);
|
||||
}
|
||||
|
||||
// the dummy device replies with its own 'done'
|
||||
@@ -565,7 +547,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(qrCodeBuffer).toBeUndefined();
|
||||
});
|
||||
|
||||
newBackendOnly("can verify another by scanning their QR code", async () => {
|
||||
it("can verify another by scanning their QR code", async () => {
|
||||
aliceClient = await startTestClient();
|
||||
// we need cross-signing keys for a QR code verification
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
@@ -650,7 +632,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.verifier).toBeUndefined();
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// ... but Alice wants to do an SAS verification
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
@@ -695,7 +677,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.verifier).toBeUndefined();
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// ... but the dummy device wants to do an SAS verification
|
||||
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
|
||||
@@ -748,6 +730,35 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request.cancellingUserId).toEqual("@alice:localhost");
|
||||
});
|
||||
|
||||
it("does not include cancelled requests in the list of requests", async () => {
|
||||
// Given Alice started a verification request
|
||||
const [, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId!;
|
||||
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
|
||||
// Sanity: the request is listed
|
||||
const requestsBeforeCancel = aliceClient
|
||||
.getCrypto()!
|
||||
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
|
||||
|
||||
expect(requestsBeforeCancel).toHaveLength(1);
|
||||
|
||||
// When Alice cancels it
|
||||
await Promise.all([expectSendToDeviceMessage("m.key.verification.cancel"), request.cancel()]);
|
||||
|
||||
// Then it is no longer listed as in progress
|
||||
const requestsAfterCancel = aliceClient
|
||||
.getCrypto()!
|
||||
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
|
||||
|
||||
expect(requestsAfterCancel).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can cancel during the SAS phase", async () => {
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
const [, request] = await Promise.all([
|
||||
@@ -774,7 +785,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
vi.advanceTimersByTime(10);
|
||||
await sendToDevicePromise;
|
||||
|
||||
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
||||
@@ -804,7 +815,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
// we should now have QR data we can display
|
||||
const qrCodeBuffer = (await request.generateQRCode())!;
|
||||
expect(qrCodeBuffer).toBeTruthy();
|
||||
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
|
||||
const sharedSecret = qrCodeBuffer.slice(74 + transactionId.length);
|
||||
|
||||
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
|
||||
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
|
||||
@@ -903,7 +914,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
describe("Send verification request in DM", () => {
|
||||
beforeEach(async () => {
|
||||
aliceClient = await startTestClient();
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
|
||||
e2eKeyResponder.addCrossSigningData(BOB_SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
e2eKeyResponder.addDeviceKeys(BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
@@ -920,19 +930,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
function awaitRoomMessageRequest(): Promise<IContent> {
|
||||
return new Promise((resolve) => {
|
||||
// Case of unencrypted message of the new crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
|
||||
(url: string, options: RequestInit) => {
|
||||
resolve(JSON.parse(options.body as string));
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
},
|
||||
);
|
||||
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
});
|
||||
|
||||
// Case of encrypted message of the old crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
|
||||
async (url: string, options: RequestInit) => {
|
||||
const encryptedMessage = JSON.parse(options.body as string);
|
||||
async (callLog) => {
|
||||
const encryptedMessage = JSON.parse(callLog.options.body as string);
|
||||
const event = new MatrixEvent({
|
||||
content: encryptedMessage,
|
||||
type: "m.room.encrypted",
|
||||
@@ -955,7 +962,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
|
||||
// 5ms by users, ie Bob and Alice
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
const messageRequestPromise = awaitRoomMessageRequest();
|
||||
const verificationRequest = await aliceClient
|
||||
@@ -986,9 +993,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
testOlmAccount.create();
|
||||
|
||||
aliceClient = await startTestClient();
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Rust crypto requires the sender's device keys before it accepts a
|
||||
// verification request.
|
||||
const crypto = aliceClient.getCrypto()!;
|
||||
|
||||
const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice");
|
||||
e2eKeyResponder.addDeviceKeys(bobDeviceKeys);
|
||||
syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } });
|
||||
await syncPromise(aliceClient);
|
||||
await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1056,7 +1072,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
});
|
||||
|
||||
it("ignores old verification requests", async () => {
|
||||
const eventHandler = jest.fn();
|
||||
const debug = vi.fn();
|
||||
const info = vi.fn();
|
||||
const warn = vi.fn();
|
||||
|
||||
// @ts-ignore overriding RustCrypto's logger
|
||||
aliceClient.getCrypto()!.logger = { debug, info, warn };
|
||||
|
||||
const eventHandler = vi.fn();
|
||||
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
|
||||
|
||||
const verificationRequestEvent = createVerificationRequestEvent();
|
||||
@@ -1070,6 +1093,16 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
|
||||
|
||||
// Wait until the request has been processed. We use a real sleep()
|
||||
// here to make sure any background async tasks are completed.
|
||||
vi.useRealTimers();
|
||||
await waitFor(async () => {
|
||||
expect(info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^Ignoring just-received verification request/),
|
||||
);
|
||||
sleep(100);
|
||||
});
|
||||
|
||||
// check that an event has not been raised, and that the request is not found
|
||||
expect(eventHandler).not.toHaveBeenCalled();
|
||||
expect(
|
||||
@@ -1077,6 +1110,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("ignores cancelled verification requests", async () => {
|
||||
// Given a verification request exists
|
||||
const event = createVerificationRequestEvent();
|
||||
returnRoomMessageFromSync(TEST_ROOM_ID, event);
|
||||
|
||||
// Wait for the request to be received
|
||||
await emitPromise(aliceClient, CryptoEvent.VerificationRequestReceived);
|
||||
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
|
||||
// When I cancel it
|
||||
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.key.verification.cancel/:id", {
|
||||
event_id: event.event_id,
|
||||
});
|
||||
await request!.cancel();
|
||||
expect(request!.phase).toEqual(VerificationPhase.Cancelled);
|
||||
|
||||
// Then it is no longer found
|
||||
expect(
|
||||
aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz"),
|
||||
).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("Plaintext verification request from Bob to Alice", async () => {
|
||||
// Add verification request from Bob to Alice in the DM between them
|
||||
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
|
||||
@@ -1121,7 +1177,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// Wait for the request to be decrypted
|
||||
const request1 = await requestEventPromise;
|
||||
@@ -1136,43 +1192,40 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(request?.otherUserId).toBe("@bob:xyz");
|
||||
});
|
||||
|
||||
newBackendOnly(
|
||||
"If the verification request is not decrypted within 5 minutes, the request is ignored",
|
||||
async () => {
|
||||
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
it("If the verification request is not decrypted within 5 minutes, the request is ignored", async () => {
|
||||
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
|
||||
// make the room_key event, but don't send it yet
|
||||
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
|
||||
|
||||
// Add verification request from Bob to Alice in the DM between them
|
||||
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
|
||||
// Add verification request from Bob to Alice in the DM between them
|
||||
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
|
||||
|
||||
// Wait for the sync response to be processed
|
||||
await syncPromise(aliceClient);
|
||||
// Wait for the sync response to be processed
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
|
||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
|
||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
// wait for a first attempt at decryption: should fail
|
||||
await awaitDecryption(matrixEvent);
|
||||
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
// wait for a first attempt at decryption: should fail
|
||||
await awaitDecryption(matrixEvent);
|
||||
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
|
||||
// Advance time by 5mins, the verification request should be ignored after that
|
||||
jest.advanceTimersByTime(5 * 60 * 1000);
|
||||
// Advance time by 5mins, the verification request should be ignored after that
|
||||
vi.advanceTimersByTime(5 * 60 * 1000);
|
||||
|
||||
// Send Bob the room keys
|
||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||
// Send Bob the room keys
|
||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||
|
||||
// Wait for the message to be decrypted
|
||||
await awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
|
||||
// Wait for the message to be decrypted
|
||||
await awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
|
||||
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
// the request should not be present
|
||||
expect(request).not.toBeDefined();
|
||||
},
|
||||
);
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
// the request should not be present
|
||||
expect(request).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Secrets are gossiped after verification", () => {
|
||||
@@ -1227,7 +1280,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
|
||||
await syncPromise(aliceClient);
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the olm device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
@@ -1239,12 +1292,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
testOlmAccount?.free();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
newBackendOnly("Should request cross signing keys after verification", async () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Should request cross signing keys after verification", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
@@ -1255,7 +1307,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
await requestPromises.get("m.cross_signing.self_signing");
|
||||
});
|
||||
|
||||
newBackendOnly("Should accept the backup decryption key gossip if valid", async () => {
|
||||
it("Should accept the backup decryption key gossip if valid", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
@@ -1274,7 +1326,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
expect(encodeBase64(cachedKey!)).toEqual(BACKUP_DECRYPTION_KEY_BASE64);
|
||||
});
|
||||
|
||||
newBackendOnly("Should not accept the backup decryption key gossip if private key do not match", async () => {
|
||||
it("Should not accept the backup decryption key gossip when there is no server-side key backup", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
|
||||
const requestId = await requestPromises.get("m.megolm_backup.v1");
|
||||
|
||||
await sendBackupGossipAndExpectVersion(
|
||||
requestId!,
|
||||
BACKUP_DECRYPTION_KEY_BASE64,
|
||||
new MatrixError({ errcode: "M_NOT_FOUND", error: "No backup found" }, 404),
|
||||
);
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
|
||||
expect(cachedKey).toBeNull();
|
||||
});
|
||||
|
||||
it("Should not accept the backup decryption key gossip when server-side key backup request errors", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
|
||||
const requestId = await requestPromises.get("m.megolm_backup.v1");
|
||||
|
||||
await sendBackupGossipAndExpectVersion(
|
||||
requestId!,
|
||||
BACKUP_DECRYPTION_KEY_BASE64,
|
||||
new Error("Network Error!"),
|
||||
);
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
|
||||
expect(cachedKey).toBeNull();
|
||||
});
|
||||
|
||||
it("Should not accept the backup decryption key gossip if private key do not match", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
@@ -1283,43 +1371,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, nonMatchingBackupInfo);
|
||||
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
jest.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
|
||||
expect(cachedKey).toBeNull();
|
||||
});
|
||||
|
||||
newBackendOnly("Should not accept the backup decryption key gossip if backup not trusted", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
|
||||
const requestId = await requestPromises.get("m.megolm_backup.v1");
|
||||
|
||||
const infoCopy = Object.assign({}, matchingBackupInfo);
|
||||
delete infoCopy.auth_data.signatures;
|
||||
|
||||
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, infoCopy);
|
||||
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
jest.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
expect(cachedKey).toBeNull();
|
||||
});
|
||||
|
||||
newBackendOnly("Should not accept the backup decryption key gossip if backup algorithm unknown", async () => {
|
||||
it("Should not accept the backup decryption key gossip if backup algorithm unknown", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
@@ -1332,19 +1389,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
unknownAlgorithmBackupInfo,
|
||||
);
|
||||
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
jest.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
|
||||
expect(cachedKey).toBeNull();
|
||||
});
|
||||
|
||||
newBackendOnly("Should not accept an invalid backup decryption key", async () => {
|
||||
it("Should not accept an invalid backup decryption key", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
@@ -1353,26 +1403,38 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
await sendBackupGossipAndExpectVersion(requestId!, "InvalidSecret", matchingBackupInfo);
|
||||
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
jest.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
const cachedKey = await retrieveBackupPrivateKeyWithDelay();
|
||||
expect(cachedKey).toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* Waits briefly for secrets to be gossipped, then fetches the backup private key from the crypto stack.
|
||||
*/
|
||||
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
vi.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
vi.useFakeTimers();
|
||||
|
||||
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Common test setup for gossiping secrets.
|
||||
* Creates a peer to peer session, sends the secret, mockup the version API, send the secret back from sync, then await for the backup check.
|
||||
*
|
||||
* @param expectBackup - The result to be returned from the `/room_keys/version` request.
|
||||
* - **KeyBackupInfo**: Indicates a successful request, where the response contains the key backup information (HTTP 200).
|
||||
* - **MatrixError**: Represents an error response from the server, indicating an unsuccessful request (non-200 HTTP status).
|
||||
* - **Error**: Indicates an error during the request process itself (e.g., network issues or unexpected failures).
|
||||
*/
|
||||
async function sendBackupGossipAndExpectVersion(
|
||||
requestId: string,
|
||||
secret: string,
|
||||
expectBackup: KeyBackupInfo,
|
||||
expectBackup: KeyBackupInfo | MatrixError | Error,
|
||||
) {
|
||||
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
|
||||
|
||||
@@ -1388,16 +1450,21 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
});
|
||||
|
||||
const expectBackupCheck = new Promise((resolve) => {
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/version",
|
||||
(url, request) => {
|
||||
resolve(undefined);
|
||||
return expectBackup;
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
|
||||
resolve(undefined);
|
||||
if (expectBackup instanceof MatrixError) {
|
||||
return {
|
||||
status: expectBackup.httpStatus,
|
||||
body: expectBackup.data,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectBackup instanceof Error) {
|
||||
return Promise.reject(expectBackup);
|
||||
}
|
||||
|
||||
return expectBackup;
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
|
||||
@@ -1464,9 +1531,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "device_under_test",
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:verification`)),
|
||||
...opts,
|
||||
});
|
||||
await initCrypto(client);
|
||||
await client.initRustCrypto();
|
||||
await client.startClient();
|
||||
return client;
|
||||
}
|
||||
@@ -1477,7 +1545,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
@@ -1511,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
|
||||
return new Promise((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
(callLog): RouteResponse => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return {};
|
||||
},
|
||||
);
|
||||
@@ -1528,40 +1596,36 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
|
||||
* @returns a map of secret name to promise that will resolve (with the id of the secret request) when the secret is requested.
|
||||
*/
|
||||
function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
|
||||
const mskRequestDefer = defer<string>();
|
||||
const sskRequestDefer = defer<string>();
|
||||
const uskRequestDefer = defer<string>();
|
||||
const backupKeyRequestDefer = defer<string>();
|
||||
const mskRequestResolvers = Promise.withResolvers<string>();
|
||||
const sskRequestResolvers = Promise.withResolvers<string>();
|
||||
const uskRequestResolvers = Promise.withResolvers<string>();
|
||||
const backupKeyRequestResolvers = Promise.withResolvers<string>();
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
if (content.action == "request") {
|
||||
const name = content.name;
|
||||
const requestId = content.request_id;
|
||||
if (name == "m.cross_signing.user_signing") {
|
||||
uskRequestDefer.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.master") {
|
||||
mskRequestDefer.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.self_signing") {
|
||||
sskRequestDefer.resolve(requestId);
|
||||
} else if (name == "m.megolm_backup.v1") {
|
||||
backupKeyRequestDefer.resolve(requestId);
|
||||
}
|
||||
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
|
||||
const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
if (content.action == "request") {
|
||||
const name = content.name;
|
||||
const requestId = content.request_id;
|
||||
if (name == "m.cross_signing.user_signing") {
|
||||
uskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.master") {
|
||||
mskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.self_signing") {
|
||||
sskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.megolm_backup.v1") {
|
||||
backupKeyRequestResolvers.resolve(requestId);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const promiseMap = new Map<string, Promise<string>>();
|
||||
promiseMap.set("m.cross_signing.master", mskRequestDefer.promise);
|
||||
promiseMap.set("m.cross_signing.self_signing", sskRequestDefer.promise);
|
||||
promiseMap.set("m.cross_signing.user_signing", uskRequestDefer.promise);
|
||||
promiseMap.set("m.megolm_backup.v1", backupKeyRequestDefer.promise);
|
||||
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
|
||||
promiseMap.set("m.cross_signing.self_signing", sskRequestResolvers.promise);
|
||||
promiseMap.set("m.cross_signing.user_signing", uskRequestResolvers.promise);
|
||||
promiseMap.set("m.megolm_backup.v1", backupKeyRequestResolvers.promise);
|
||||
return promiseMap;
|
||||
}
|
||||
|
||||
@@ -1592,7 +1656,7 @@ function sha256(commitmentStr: string): string {
|
||||
return encodeUnpaddedBase64(createHash("sha256").update(commitmentStr, "utf8").digest());
|
||||
}
|
||||
|
||||
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
function encodeUnpaddedBase64(uint8Array: ArrayLike<number>): string {
|
||||
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
@@ -1626,7 +1690,7 @@ function buildReadyMessage(
|
||||
}
|
||||
|
||||
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
|
||||
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
|
||||
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayLike<number>) {
|
||||
return {
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
@@ -1722,7 +1786,7 @@ function buildQRCode(
|
||||
key2Base64: string,
|
||||
sharedSecret: string,
|
||||
mode = 0x02,
|
||||
): Uint8Array {
|
||||
): Uint8ClampedArray {
|
||||
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
|
||||
|
||||
const qrCodeBuffer = Buffer.alloc(150); // oversize
|
||||
@@ -1738,5 +1802,5 @@ function buildQRCode(
|
||||
idx += qrCodeBuffer.write(sharedSecret, idx);
|
||||
|
||||
// truncate to the right length
|
||||
return qrCodeBuffer.subarray(0, idx);
|
||||
return new Uint8ClampedArray(qrCodeBuffer.subarray(0, idx));
|
||||
}
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from "../TestClient";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { logger } from "../../src/logger";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single e2e room (ROOM_ID), with the
|
||||
* members given
|
||||
*
|
||||
* @returns sync response
|
||||
*/
|
||||
function getSyncResponse(roomMembers: string[]) {
|
||||
const stateEvents = [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encryption",
|
||||
skey: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
Array.prototype.push.apply(
|
||||
stateEvents,
|
||||
roomMembers.map((m) =>
|
||||
testUtils.mkMembership({
|
||||
mship: KnownMembership.Join,
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
state: {
|
||||
events: stateEvents,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
describe("DeviceList management:", function () {
|
||||
if (!global.Olm) {
|
||||
logger.warn("not running deviceList tests: Olm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
let aliceTestClient: TestClient;
|
||||
let sessionStoreBackend: Storage;
|
||||
|
||||
async function createTestClient() {
|
||||
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
|
||||
await testClient.client.initCrypto();
|
||||
return testClient;
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
|
||||
aliceTestClient = await createTestClient();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function () {
|
||||
aliceTestClient.expectKeyQuery({
|
||||
device_keys: { "@alice:localhost": {} },
|
||||
failures: {},
|
||||
});
|
||||
return aliceTestClient
|
||||
.start()
|
||||
.then(function () {
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
})
|
||||
.then(function () {
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(["@bob:xyz"]),
|
||||
aliceTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
})
|
||||
.then(function () {
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, {
|
||||
event_id: "$event_id",
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("We should not get confused by out-of-order device query responses", () => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({
|
||||
device_keys: { "@alice:localhost": {} },
|
||||
failures: {},
|
||||
});
|
||||
return aliceTestClient
|
||||
.start()
|
||||
.then(() => {
|
||||
aliceTestClient.httpBackend
|
||||
.when("GET", "/sync")
|
||||
.respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"]));
|
||||
return aliceTestClient.flushSync();
|
||||
})
|
||||
.then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
"@chris:abc": {},
|
||||
},
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
|
||||
aliceTestClient.httpBackend
|
||||
.flush("/keys/query", 1)
|
||||
.then(() => aliceTestClient.httpBackend.flush("/send/", 1)),
|
||||
aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
|
||||
]);
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data!.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "2",
|
||||
device_lists: {
|
||||
changed: ["@bob:xyz"],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "3",
|
||||
device_lists: {
|
||||
changed: ["@chris:abc"],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend
|
||||
.when("POST", "/keys/query", {
|
||||
device_keys: {
|
||||
"@chris:abc": {},
|
||||
},
|
||||
token: "3",
|
||||
})
|
||||
.respond(200, {
|
||||
device_keys: { "@chris:abc": {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error("Unexpected status for bob: wanted 1 or 2, got " + bobStat);
|
||||
}
|
||||
const chrisStat = data!.trackingStatus["@chris:abc"];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + chrisStat);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend
|
||||
.when("POST", "/keys/query", {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
token: "2",
|
||||
})
|
||||
.respond(200, {
|
||||
device_keys: { "@bob:xyz": {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(["@bob:xyz"]);
|
||||
})
|
||||
.then(() => {
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data!.trackingStatus["@chris:abc"];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + bobStat);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
})
|
||||
.then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(["@chris:abc"]);
|
||||
})
|
||||
.then(() => {
|
||||
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
})
|
||||
.then(() => {
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
const chrisStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data!.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function () {
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
|
||||
device_keys: {
|
||||
"@bob:xyz": {},
|
||||
},
|
||||
});
|
||||
await aliceTestClient.httpBackend.flush("/keys/query", 1);
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should be tracking bob's device list
|
||||
expect(bobStat).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves", async function () {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ["@bob:xyz"],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: KnownMembership.Leave,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Alice leaves", async function () {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ["@bob:xyz"],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: KnownMembership.Leave,
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
|
||||
// @ts-ignore accessing a protected field
|
||||
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves whilst Alice is offline", async function () {
|
||||
aliceTestClient.stop();
|
||||
|
||||
const anotherTestClient = await createTestClient();
|
||||
|
||||
try {
|
||||
await anotherTestClient.start();
|
||||
anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
|
||||
|
||||
// @ts-ignore accessing private property
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data!.trackingStatus["@bob:xyz"];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
expect(bobStat).toEqual(0);
|
||||
});
|
||||
} finally {
|
||||
anotherTestClient.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import {
|
||||
ClientEvent,
|
||||
HttpApiEvent,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
type IEvent,
|
||||
type MatrixClient,
|
||||
RoomEvent,
|
||||
RoomMemberEvent,
|
||||
RoomStateEvent,
|
||||
|
||||
@@ -23,15 +23,15 @@ import {
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
type IEvent,
|
||||
type MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/utils";
|
||||
import { encodeParams, encodeUri, type QueryDict, replaceParam } from "../../src/utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
it("should return null when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
it("should return null when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
@@ -1144,7 +1144,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
@@ -1155,7 +1155,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
@@ -1247,7 +1247,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
await room.addLiveEvents([THREAD_REPLY3]);
|
||||
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
@@ -1560,7 +1560,7 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.NewReply);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
@@ -1685,7 +1685,7 @@ describe("MatrixClient event timelines", function () {
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION], { addToState: false });
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
|
||||
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
|
||||
@@ -13,19 +13,26 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
|
||||
import { type IStoredClientOpts, MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
|
||||
import {
|
||||
Filter,
|
||||
JoinRule,
|
||||
type KnockRoomOpts,
|
||||
MemoryStore,
|
||||
Method,
|
||||
Room,
|
||||
type RoomSummary,
|
||||
SERVICE_TYPES,
|
||||
} from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { IFilterDefinition } from "../../src/filter";
|
||||
import { ISearchResults } from "../../src/@types/search";
|
||||
import { IStore } from "../../src/store";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { type IFilterDefinition } from "../../src/filter";
|
||||
import { type ISearchResults } from "../../src/@types/search";
|
||||
import { type IStore } from "../../src/store";
|
||||
import { SetPresence } from "../../src/sync";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
@@ -150,6 +157,30 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("mediaConfig", function () {
|
||||
it("should get media config on unauthenticated media call", async () => {
|
||||
httpBackend.when("GET", "/_matrix/media/v3/config").respond(200, '{"m.upload.size": 50000000}', true);
|
||||
|
||||
const prom = client.getMediaConfig();
|
||||
|
||||
httpBackend.flushAllExpected();
|
||||
|
||||
expect((await prom)["m.upload.size"]).toEqual(50000000);
|
||||
});
|
||||
|
||||
it("should get media config on authenticated media call", async () => {
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/v1/media/config")
|
||||
.respond(200, '{"m.upload.size": 50000000}', true);
|
||||
|
||||
const prom = client.getMediaConfig(true);
|
||||
|
||||
httpBackend.flushAllExpected();
|
||||
|
||||
expect((await prom)["m.upload.size"]).toEqual(50000000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("joinRoom", function () {
|
||||
it("should no-op given the ID of a room you've already joined", async () => {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -159,14 +190,17 @@ describe("MatrixClient", function () {
|
||||
type: "test",
|
||||
content: {},
|
||||
});
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
store.storeRoom(room);
|
||||
|
||||
@@ -179,14 +213,17 @@ describe("MatrixClient", function () {
|
||||
const roomId = "!roomId:server";
|
||||
const roomAlias = "#my-fancy-room:server";
|
||||
const room = new Room(roomId, client, userId);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
store.storeRoom(room);
|
||||
|
||||
// The method makes a request to resolve the alias
|
||||
@@ -230,6 +267,59 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("invite", function () {
|
||||
it("should send request to /invite", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const userId = "@user:server";
|
||||
|
||||
httpBackend
|
||||
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ user_id: userId });
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
const prom = client.invite(roomId, userId);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it("accepts a stringy reason argument", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const userId = "@user:server";
|
||||
|
||||
httpBackend
|
||||
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ user_id: userId, reason: "testreason" });
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
const prom = client.invite(roomId, userId, "testreason");
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it("accepts an options object with a reason", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const userId = "@user:server";
|
||||
|
||||
httpBackend
|
||||
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ user_id: userId, reason: "testreason" });
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
const prom = client.invite(roomId, userId, { reason: "testreason" });
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
});
|
||||
|
||||
describe("knockRoom", function () {
|
||||
const roomId = "!some-room-id:example.org";
|
||||
const reason = "some reason";
|
||||
@@ -248,7 +338,7 @@ describe("MatrixClient", function () {
|
||||
.when("POST", "/knock/" + encodeURIComponent(roomId))
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ reason: opts.reason });
|
||||
expect(request.queryParams).toEqual({ server_name: opts.viaServers });
|
||||
expect(request.queryParams).toEqual({ server_name: opts.viaServers, via: opts.viaServers });
|
||||
})
|
||||
.respond(200, { room_id: roomId });
|
||||
|
||||
@@ -257,6 +347,7 @@ describe("MatrixClient", function () {
|
||||
expect((await prom).room_id).toBe(roomId);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should no-op if you've already knocked a room", function () {
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
@@ -266,14 +357,17 @@ describe("MatrixClient", function () {
|
||||
content: {},
|
||||
});
|
||||
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Knock,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Knock,
|
||||
event: true,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
store.storeRoom(room);
|
||||
@@ -287,23 +381,16 @@ describe("MatrixClient", function () {
|
||||
[
|
||||
403,
|
||||
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
|
||||
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
|
||||
],
|
||||
[
|
||||
500,
|
||||
{ errcode: "INTERNAL_SERVER_ERROR" },
|
||||
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
|
||||
"MatrixError: [403] You don't have permission to knock",
|
||||
],
|
||||
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
|
||||
];
|
||||
|
||||
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
|
||||
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
|
||||
|
||||
const prom = client.knockRoom(roomId);
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
|
||||
]);
|
||||
await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -626,126 +713,6 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadKeys", function () {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
// running initCrypto should trigger a key upload
|
||||
httpBackend.when("POST", "/keys/upload").respond(200, {});
|
||||
return Promise.all([client.initCrypto(), httpBackend.flush("/keys/upload", 1)]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should do an HTTP request and then store the keys", function () {
|
||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
const borisKeys = {
|
||||
dev1: {
|
||||
algorithms: ["1"],
|
||||
device_id: "dev1",
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
signatures: {
|
||||
boris: {
|
||||
"ed25519:dev1":
|
||||
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
|
||||
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
|
||||
},
|
||||
},
|
||||
unsigned: { abc: "def" },
|
||||
user_id: "boris",
|
||||
},
|
||||
};
|
||||
const chazKeys = {
|
||||
dev2: {
|
||||
algorithms: ["2"],
|
||||
device_id: "dev2",
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
chaz: {
|
||||
"ed25519:dev2":
|
||||
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
|
||||
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
|
||||
},
|
||||
},
|
||||
unsigned: { ghi: "def" },
|
||||
user_id: "chaz",
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
function sign(o) {
|
||||
var anotherjson = require('another-json');
|
||||
var b = JSON.parse(JSON.stringify(o));
|
||||
delete(b.signatures);
|
||||
delete(b.unsigned);
|
||||
return client.crypto.olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
logger.log("Ed25519: " + ed25519key);
|
||||
logger.log("boris:", sign(borisKeys.dev1));
|
||||
logger.log("chaz:", sign(chazKeys.dev2));
|
||||
*/
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/keys/query")
|
||||
.check(function (req) {
|
||||
expect(req.data).toEqual({
|
||||
device_keys: {
|
||||
boris: [],
|
||||
chaz: [],
|
||||
},
|
||||
});
|
||||
})
|
||||
.respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
chaz: chazKeys,
|
||||
},
|
||||
});
|
||||
|
||||
const prom = client.downloadKeys(["boris", "chaz"]).then(function (res) {
|
||||
assertObjectContains(res.get("boris")!.get("dev1")!, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { abc: "def" },
|
||||
});
|
||||
|
||||
assertObjectContains(res.get("chaz")!.get("dev2")!, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
algorithms: ["2"],
|
||||
unsigned: { ghi: "def" },
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDevice", function () {
|
||||
const auth = { identifier: 1 };
|
||||
it("should pass through an auth dict", function () {
|
||||
httpBackend
|
||||
.when("DELETE", "/_matrix/client/v3/devices/my_device")
|
||||
.check(function (req) {
|
||||
expect(req.data).toEqual({ auth: auth });
|
||||
})
|
||||
.respond(200);
|
||||
|
||||
const prom = client.deleteDevice("my_device", auth);
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("partitionThreadedEvents", function () {
|
||||
let room: Room;
|
||||
beforeEach(() => {
|
||||
@@ -1225,7 +1192,7 @@ describe("MatrixClient", function () {
|
||||
describe("logout", () => {
|
||||
it("should abort pending requests when called with stopClient=true", async () => {
|
||||
httpBackend.when("POST", "/logout").respond(200, {});
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
client.http.request(Method.Get, "/test").catch(fn);
|
||||
client.logout(true);
|
||||
await httpBackend.flush(undefined);
|
||||
@@ -1284,18 +1251,109 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("getCapabilities", () => {
|
||||
it("should cache by default", async () => {
|
||||
it("should return cached capabilities if present", async () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: {
|
||||
"m.change_password": false,
|
||||
},
|
||||
capabilities: capsObject,
|
||||
});
|
||||
const prom = httpBackend.flushAllExpected();
|
||||
const capabilities1 = await client.getCapabilities();
|
||||
const capabilities2 = await client.getCapabilities();
|
||||
|
||||
client.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await client.getCapabilities()).toEqual(capsObject);
|
||||
});
|
||||
|
||||
it("should fetch capabilities if cache not present", async () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: capsObject,
|
||||
});
|
||||
|
||||
const capsPromise = client.getCapabilities();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await capsPromise).toEqual(capsObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCachedCapabilities", () => {
|
||||
it("should return cached capabilities or undefined", async () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: capsObject,
|
||||
});
|
||||
|
||||
expect(client.getCachedCapabilities()).toBeUndefined();
|
||||
|
||||
client.startClient();
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(client.getCachedCapabilities()).toEqual(capsObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchCapabilities", () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: capsObject,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should always fetch capabilities and then cache", async () => {
|
||||
const prom = client.fetchCapabilities();
|
||||
await httpBackend.flushAllExpected();
|
||||
const caps = await prom;
|
||||
|
||||
expect(caps).toEqual(capsObject);
|
||||
});
|
||||
|
||||
it("should write-through the cache", async () => {
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
client.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(client.getCachedCapabilities()).toEqual(capsObject);
|
||||
|
||||
const newCapsObject = {
|
||||
"m.change_password": true,
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: newCapsObject,
|
||||
});
|
||||
|
||||
const prom = client.fetchCapabilities();
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
|
||||
expect(capabilities1).toStrictEqual(capabilities2);
|
||||
expect(client.getCachedCapabilities()).toEqual(newCapsObject);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1333,6 +1391,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("publicRooms", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should use GET request if no server or filter is specified", () => {
|
||||
httpBackend.when("GET", "/publicRooms").respond(200, {});
|
||||
client.publicRooms({});
|
||||
@@ -1519,52 +1578,9 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("uploadKeys", () => {
|
||||
// uploadKeys() is a no-op nowadays, so there's not much to test here.
|
||||
it("should complete successfully", async () => {
|
||||
await client.uploadKeys();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCryptoTrustCrossSignedDevices", () => {
|
||||
it("should throw if e2e is disabled", () => {
|
||||
expect(() => client.getCryptoTrustCrossSignedDevices()).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("should proxy to the crypto backend", async () => {
|
||||
const mockBackend = {
|
||||
getTrustCrossSignedDevices: jest.fn().mockReturnValue(true),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client["cryptoBackend"] = mockBackend;
|
||||
|
||||
expect(client.getCryptoTrustCrossSignedDevices()).toBe(true);
|
||||
mockBackend.getTrustCrossSignedDevices.mockReturnValue(false);
|
||||
expect(client.getCryptoTrustCrossSignedDevices()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCryptoTrustCrossSignedDevices", () => {
|
||||
it("should throw if e2e is disabled", () => {
|
||||
expect(() => client.setCryptoTrustCrossSignedDevices(false)).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("should proxy to the crypto backend", async () => {
|
||||
const mockBackend = {
|
||||
setTrustCrossSignedDevices: jest.fn(),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client["cryptoBackend"] = mockBackend;
|
||||
|
||||
client.setCryptoTrustCrossSignedDevices(true);
|
||||
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(true);
|
||||
|
||||
client.setCryptoTrustCrossSignedDevices(false);
|
||||
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSyncPresence", () => {
|
||||
it("should pass calls through to the underlying sync api", () => {
|
||||
const setPresence = jest.fn();
|
||||
const setPresence = vi.fn();
|
||||
// @ts-ignore
|
||||
client.syncApi = { setPresence };
|
||||
client.setSyncPresence(SetPresence.Unavailable);
|
||||
@@ -1573,6 +1589,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("sendTyping", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should bail early for guests", async () => {
|
||||
client.setGuest(true);
|
||||
await client.sendTyping("!room:server", true, 100);
|
||||
@@ -1710,6 +1727,146 @@ describe("MatrixClient", function () {
|
||||
await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoomSummary", () => {
|
||||
const roomId = "!foo:bar";
|
||||
const encodedRoomId = encodeURIComponent(roomId);
|
||||
|
||||
const roomSummary: RoomSummary = {
|
||||
"room_id": roomId,
|
||||
"name": "My Room",
|
||||
"avatar_url": "",
|
||||
"topic": "My room topic",
|
||||
"world_readable": false,
|
||||
"guest_can_join": false,
|
||||
"num_joined_members": 1,
|
||||
"room_type": "",
|
||||
"join_rule": JoinRule.Public,
|
||||
"membership": "leave",
|
||||
"im.nheko.summary.room_version": "6",
|
||||
"im.nheko.summary.encryption": "algo",
|
||||
};
|
||||
|
||||
const prefix = "/_matrix/client/unstable/im.nheko.summary/";
|
||||
const suffix = `summary/${encodedRoomId}`;
|
||||
const deprecatedSuffix = `rooms/${encodedRoomId}/summary`;
|
||||
|
||||
const errorUnrecogStatus = 404;
|
||||
const errorUnrecogBody = {
|
||||
errcode: "M_UNRECOGNIZED",
|
||||
error: "Unsupported endpoint",
|
||||
};
|
||||
|
||||
const errorBadreqStatus = 400;
|
||||
const errorBadreqBody = {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Invalid request",
|
||||
};
|
||||
|
||||
it("should respond with a valid room summary object", () => {
|
||||
httpBackend.when("GET", prefix + suffix).respond(200, roomSummary);
|
||||
|
||||
const prom = client.getRoomSummary(roomId).then((response) => {
|
||||
expect(response).toEqual(roomSummary);
|
||||
});
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should allow fallback to the deprecated endpoint", () => {
|
||||
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
|
||||
httpBackend.when("GET", prefix + deprecatedSuffix).respond(200, roomSummary);
|
||||
|
||||
const prom = client.getRoomSummary(roomId).then((response) => {
|
||||
expect(response).toEqual(roomSummary);
|
||||
});
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should respond to unsupported path with error", () => {
|
||||
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
|
||||
httpBackend.when("GET", prefix + deprecatedSuffix).respond(errorUnrecogStatus, errorUnrecogBody);
|
||||
|
||||
const prom = client.getRoomSummary(roomId).then(
|
||||
function (response) {
|
||||
throw Error("request not failed");
|
||||
},
|
||||
function (error) {
|
||||
expect(error.httpStatus).toEqual(errorUnrecogStatus);
|
||||
expect(error.errcode).toEqual(errorUnrecogBody.errcode);
|
||||
expect(error.message).toEqual(`MatrixError: [${errorUnrecogStatus}] ${errorUnrecogBody.error}`);
|
||||
},
|
||||
);
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should respond to invalid path arguments with error", () => {
|
||||
httpBackend.when("GET", prefix).respond(errorBadreqStatus, errorBadreqBody);
|
||||
|
||||
const prom = client.getRoomSummary("notAroom").then(
|
||||
function (response) {
|
||||
throw Error("request not failed");
|
||||
},
|
||||
function (error) {
|
||||
expect(error.httpStatus).toEqual(errorBadreqStatus);
|
||||
expect(error.errcode).toEqual(errorBadreqBody.errcode);
|
||||
expect(error.message).toEqual(`MatrixError: [${errorBadreqStatus}] ${errorBadreqBody.error}`);
|
||||
},
|
||||
);
|
||||
|
||||
httpBackend.flush("");
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDomain", () => {
|
||||
it("should return null if no userId is set", () => {
|
||||
const client = new MatrixClient({ baseUrl: "http://localhost" });
|
||||
expect(client.getDomain()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the domain of the userId", () => {
|
||||
expect(client.getDomain()).toBe("localhost");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserIdLocalpart", () => {
|
||||
it("should return null if no userId is set", () => {
|
||||
const client = new MatrixClient({ baseUrl: "http://localhost" });
|
||||
expect(client.getUserIdLocalpart()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the localpart of the userId", () => {
|
||||
expect(client.getUserIdLocalpart()).toBe("alice");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setRoomMutePushRule", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should set room push rule to muted", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "http://localhost",
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
});
|
||||
client.pushRules = {
|
||||
global: {
|
||||
room: [{ rule_id: roomId, actions: [], default: false, enabled: false }],
|
||||
},
|
||||
};
|
||||
|
||||
const path = `/pushrules/global/room/${encodeURIComponent(roomId)}`;
|
||||
httpBackend.when("DELETE", path).respond(200, {});
|
||||
httpBackend.when("PUT", path).respond(200, {});
|
||||
client.setRoomMutePushRule("global", roomId, true);
|
||||
await httpBackend.flush("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
@@ -1720,7 +1877,6 @@ function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
|
||||
const buildEventMessageInThread = (root: MatrixEvent) =>
|
||||
new MatrixEvent({
|
||||
age: 80098509,
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
@@ -1741,12 +1897,10 @@ const buildEventMessageInThread = (root: MatrixEvent) =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80098509 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPollResponseReference = () =>
|
||||
new MatrixEvent({
|
||||
age: 80098509,
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
@@ -1764,7 +1918,6 @@ const buildEventPollResponseReference = () =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80106237 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventReaction = (event: MatrixEvent) =>
|
||||
@@ -1804,7 +1957,6 @@ const buildEventRedaction = (event: MatrixEvent) =>
|
||||
|
||||
const buildEventPollStartThreadRoot = () =>
|
||||
new MatrixEvent({
|
||||
age: 80108647,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "ENCRYPTEDSTUFF",
|
||||
@@ -1818,12 +1970,10 @@ const buildEventPollStartThreadRoot = () =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80108647 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventReply = (target: MatrixEvent) =>
|
||||
new MatrixEvent({
|
||||
age: 80098509,
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "ENCRYPTEDSTUFF",
|
||||
@@ -1842,12 +1992,10 @@ const buildEventReply = (target: MatrixEvent) =>
|
||||
sender: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: { age: 80098509 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventRoomName = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123249,
|
||||
content: {
|
||||
name: "1 poll, 1 vote, 1 thread",
|
||||
},
|
||||
@@ -1858,12 +2006,10 @@ const buildEventRoomName = () =>
|
||||
state_key: "",
|
||||
type: "m.room.name",
|
||||
unsigned: { age: 80123249 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventEncryption = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123383,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
@@ -1874,12 +2020,10 @@ const buildEventEncryption = () =>
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
unsigned: { age: 80123383 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventGuestAccess = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123473,
|
||||
content: {
|
||||
guest_access: "can_join",
|
||||
},
|
||||
@@ -1890,12 +2034,10 @@ const buildEventGuestAccess = () =>
|
||||
state_key: "",
|
||||
type: "m.room.guest_access",
|
||||
unsigned: { age: 80123473 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventHistoryVisibility = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123556,
|
||||
content: {
|
||||
history_visibility: "shared",
|
||||
},
|
||||
@@ -1906,12 +2048,10 @@ const buildEventHistoryVisibility = () =>
|
||||
state_key: "",
|
||||
type: "m.room.history_visibility",
|
||||
unsigned: { age: 80123556 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventJoinRules = () =>
|
||||
new MatrixEvent({
|
||||
age: 80123696,
|
||||
content: {
|
||||
join_rule: KnownMembership.Invite,
|
||||
},
|
||||
@@ -1922,12 +2062,10 @@ const buildEventJoinRules = () =>
|
||||
state_key: "",
|
||||
type: "m.room.join_rules",
|
||||
unsigned: { age: 80123696 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventPowerLevels = () =>
|
||||
new MatrixEvent({
|
||||
age: 80124105,
|
||||
content: {
|
||||
ban: 50,
|
||||
events: {
|
||||
@@ -1958,12 +2096,10 @@ const buildEventPowerLevels = () =>
|
||||
state_key: "",
|
||||
type: "m.room.power_levels",
|
||||
unsigned: { age: 80124105 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventMember = () =>
|
||||
new MatrixEvent({
|
||||
age: 80125279,
|
||||
content: {
|
||||
avatar_url: "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
|
||||
displayname: "andybalaam-test1",
|
||||
@@ -1976,12 +2112,10 @@ const buildEventMember = () =>
|
||||
state_key: "@andybalaam-test1:matrix.org",
|
||||
type: "m.room.member",
|
||||
unsigned: { age: 80125279 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
const buildEventCreate = () =>
|
||||
new MatrixEvent({
|
||||
age: 80126105,
|
||||
content: {
|
||||
room_version: "6",
|
||||
},
|
||||
@@ -1992,13 +2126,4 @@ const buildEventCreate = () =>
|
||||
state_key: "",
|
||||
type: "m.room.create",
|
||||
unsigned: { age: 80126105 },
|
||||
user_id: "@andybalaam-test1:matrix.org",
|
||||
});
|
||||
|
||||
function assertObjectContains(obj: Record<string, any>, expected: any): void {
|
||||
for (const k in expected) {
|
||||
if (expected.hasOwnProperty(k)) {
|
||||
expect(obj[k]).toEqual(expected[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ClientEvent, MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { IStore } from "../../src/store";
|
||||
import { type IStore } from "../../src/store";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
describe("MatrixClient opts", function () {
|
||||
@@ -80,7 +80,7 @@ describe("MatrixClient opts", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store: undefined,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@@ -135,7 +135,7 @@ describe("MatrixClient opts", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
|
||||
|
||||
await expect(
|
||||
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
|
||||
).rejects.toThrow("MatrixError: [500] Unknown message");
|
||||
).rejects.toThrow("MatrixError: [500] Ruh roh");
|
||||
});
|
||||
|
||||
it("shouldn't queue events", async () => {
|
||||
@@ -205,4 +205,109 @@ describe("MatrixClient opts", function () {
|
||||
expect(res.event_id).toEqual("foo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with opts.queryParams", function () {
|
||||
let client: MatrixClient;
|
||||
let httpBackend: HttpBackend;
|
||||
const userId = "@rsb-tbg:localhost";
|
||||
|
||||
beforeEach(function () {
|
||||
httpBackend = new HttpBackend();
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
queryParams: { user_id: userId },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
client.stopClient();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
it("should include queryParams in matrix server requests", async () => {
|
||||
const eventId = "$test:event";
|
||||
httpBackend
|
||||
.when("PUT", "/txn1")
|
||||
.check((req) => {
|
||||
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
|
||||
return true;
|
||||
})
|
||||
.respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "test message", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
});
|
||||
|
||||
it("should include queryParams in sync requests", async () => {
|
||||
httpBackend
|
||||
.when("GET", "/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
|
||||
return true;
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/pushrules")
|
||||
.check((req) => {
|
||||
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
|
||||
return true;
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
httpBackend
|
||||
.when("POST", "/filter")
|
||||
.check((req) => {
|
||||
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
|
||||
return true;
|
||||
})
|
||||
.respond(200, { filter_id: "foo" });
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
|
||||
return true;
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
await httpBackend.flush("/versions", 1);
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
|
||||
});
|
||||
|
||||
it("should merge queryParams with request-specific params", async () => {
|
||||
const eventId = "$test:event";
|
||||
httpBackend
|
||||
.when("PUT", "/txn1")
|
||||
.check((req) => {
|
||||
// Should contain both global queryParams and request-specific params
|
||||
expect(req.path).toContain(`user_id=${encodeURIComponent(userId)}`);
|
||||
return true;
|
||||
})
|
||||
.respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "test message", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,9 +15,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import { Direction, type MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient relations", () => {
|
||||
|
||||
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import { EventStatus, type MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
@@ -60,7 +59,7 @@ describe("MatrixClient retrying", function () {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client!
|
||||
.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: "m1",
|
||||
})
|
||||
.then(
|
||||
@@ -77,7 +76,7 @@ describe("MatrixClient retrying", function () {
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client!.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: "m2",
|
||||
});
|
||||
|
||||
|
||||
@@ -14,20 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { EventStatus } from "../../src/models/event";
|
||||
import {
|
||||
MatrixError,
|
||||
ClientEvent,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
type IEvent,
|
||||
type MatrixClient,
|
||||
RoomEvent,
|
||||
ISyncResponse,
|
||||
IMinimalEvent,
|
||||
IRoomEvent,
|
||||
Room,
|
||||
type ISyncResponse,
|
||||
type IMinimalEvent,
|
||||
type IRoomEvent,
|
||||
type Room,
|
||||
} from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
@@ -333,7 +332,7 @@ describe("MatrixClient room timelines", function () {
|
||||
name: userName,
|
||||
url: "mxc://some/url",
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
oldMshipEvent.unsigned!.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: undefined,
|
||||
membership: KnownMembership.Join,
|
||||
@@ -721,7 +720,7 @@ describe("MatrixClient room timelines", function () {
|
||||
} else {
|
||||
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
|
||||
}
|
||||
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
|
||||
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Vitest? */);
|
||||
|
||||
room.on(RoomEvent.TimelineReset, async () => {
|
||||
try {
|
||||
|
||||
@@ -15,9 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
|
||||
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
|
||||
|
||||
const makeQueryablePromise = <T = void>(promise: Promise<T>) => {
|
||||
let resolved = false;
|
||||
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
|
||||
});
|
||||
|
||||
it("should retry, until errors are solved.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
vi.useFakeTimers();
|
||||
fetchMock
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
|
||||
@@ -105,20 +104,20 @@ describe("MatrixClient syncing errors", () => {
|
||||
|
||||
await client!.startClient();
|
||||
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[1].promise).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("should stop sync keep alive when client is stopped.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
vi.useFakeTimers();
|
||||
fetchMock
|
||||
.get("end:capabilities", {})
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
|
||||
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
|
||||
@@ -145,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
|
||||
|
||||
const syntState = await firstSyncEvent.promise;
|
||||
expect(syntState).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
|
||||
vi.runAllTimers(); // this will skip forward to trigger the keepAlive
|
||||
|
||||
jest.useRealTimers(); // we need real timer for the setTimout below to work
|
||||
vi.useRealTimers(); // we need real timer for the setTimout below to work
|
||||
|
||||
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ limitations under the License.
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import {
|
||||
EventTimeline,
|
||||
MatrixEvent,
|
||||
@@ -25,20 +24,20 @@ import {
|
||||
RoomStateEvent,
|
||||
RoomMemberEvent,
|
||||
UNSTABLE_MSC2716_MARKER,
|
||||
MatrixClient,
|
||||
type MatrixClient,
|
||||
ClientEvent,
|
||||
IndexedDBCryptoStore,
|
||||
ISyncResponse,
|
||||
IRoomEvent,
|
||||
IJoinedRoom,
|
||||
IStateEvent,
|
||||
IMinimalEvent,
|
||||
type ISyncResponse,
|
||||
type IRoomEvent,
|
||||
type IJoinedRoom,
|
||||
type IStateEvent,
|
||||
type IMinimalEvent,
|
||||
NotificationCountType,
|
||||
IEphemeral,
|
||||
type IEphemeral,
|
||||
Room,
|
||||
IndexedDBStore,
|
||||
RelationType,
|
||||
EventType,
|
||||
MatrixEventEvent,
|
||||
} from "../../src";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||
@@ -46,8 +45,16 @@ import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
|
||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { type IActionsObject } from "../../src/pushprocessor";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
|
||||
declare module "../../src/@types/event" {
|
||||
interface AccountDataEvents {
|
||||
a: {};
|
||||
b: {};
|
||||
}
|
||||
}
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
@@ -87,6 +94,7 @@ describe("MatrixClient syncing", () => {
|
||||
presence: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should /sync after /pushrules and /filter.", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
@@ -110,7 +118,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
await client!.initRustCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -225,7 +233,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
await client!.initRustCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -494,7 +502,7 @@ describe("MatrixClient syncing", () => {
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
return httpBackend!.flushAllExpected();
|
||||
@@ -554,7 +562,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should resolve incoming invites from /sync", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -575,7 +583,7 @@ describe("MatrixClient syncing", () => {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(member.getAvatarUrl("home.server.url", 1, 1, "", false, false)).toBeTruthy();
|
||||
expect(member.getAvatarUrl("https://home.server.url", 1, 1, "", false, false)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -587,7 +595,7 @@ describe("MatrixClient syncing", () => {
|
||||
name: "The Ghost",
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -615,7 +623,7 @@ describe("MatrixClient syncing", () => {
|
||||
name: "The Ghost",
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -642,7 +650,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should no-op if resolveInvitesToProfiles is not set", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -987,7 +995,7 @@ describe("MatrixClient syncing", () => {
|
||||
roomVersion: "org.matrix.msc2716v3",
|
||||
},
|
||||
].forEach((testMeta) => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
// eslint-disable-next-line @vitest/valid-title
|
||||
describe(testMeta.label, () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
@@ -1371,6 +1379,114 @@ describe("MatrixClient syncing", () => {
|
||||
expect(stateEventEmitCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("msc4222", () => {
|
||||
const roomOneSyncOne = {
|
||||
"timeline": {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
content: {
|
||||
name: "Initial room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
const roomOneSyncTwo = {
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.topic",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { topic: "A new room topic" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
"state": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { name: "A new room name" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should ignore state events in timeline when state_after is present", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncOne },
|
||||
},
|
||||
});
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncTwo },
|
||||
},
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.name).toEqual("Initial room name");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect state events in state_after for left rooms", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncOne },
|
||||
},
|
||||
});
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
leave: { [roomOne]: roomOneSyncTwo },
|
||||
},
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.name).toEqual("Initial room name");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline", () => {
|
||||
@@ -1646,6 +1762,99 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should zero total notifications for threads when absent from the notifications object", async () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 2,
|
||||
notification_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
|
||||
it("should zero highlight notifications for threads in encrypted rooms", async () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 2,
|
||||
notification_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 0,
|
||||
notification_count: 0,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("should not zero highlight notifications for threads in encrypted rooms", async () => {
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 2,
|
||||
notification_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
room!.hasEncryptionStateEvent = vi.fn().mockReturnValue(true);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
|
||||
[THREAD_ID]: {
|
||||
highlight_count: 0,
|
||||
notification_count: 0,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
|
||||
});
|
||||
|
||||
it("caches unknown threads receipts and replay them when the thread is created", async () => {
|
||||
const THREAD_ID = "$unknownthread:localhost";
|
||||
|
||||
@@ -1733,64 +1942,351 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
||||
const roomId = "!room123:server";
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [],
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
room: roomId,
|
||||
event: true,
|
||||
skey: "",
|
||||
type: EventType.RoomEncryption,
|
||||
content: {},
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
describe("encrypted notification logic", () => {
|
||||
let roomId: string;
|
||||
let syncData: ISyncResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
roomId = "!room123:server";
|
||||
syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [],
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
room: roomId,
|
||||
event: true,
|
||||
skey: "",
|
||||
type: EventType.RoomEncryption,
|
||||
content: {},
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
user: selfUserId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
} as unknown as ISyncResponse;
|
||||
});
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
it("should apply encrypted notification logic for events within the same sync blob", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
|
||||
it("should recalculate highlights on unthreaded receipt for encrypted rooms", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
|
||||
|
||||
// add a receipt for the first event in the room (let's say the user has already read that one)
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
// Now add a highlighting event after that receipt
|
||||
const pingEvent = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: client?.getUserId() + " ping",
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
|
||||
|
||||
// fudge this to make it a highlight
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if (ev.getId() === pingEvent.event_id) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
// the room should now have one highlight since our receipt was before the ping message
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("should recalculate highlights on main thread receipt for encrypted rooms", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
|
||||
|
||||
// add a receipt for the first event in the room (let's say the user has already read that one)
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1, thread_id: "main" },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
// Now add a highlighting event after that receipt
|
||||
const pingEvent = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: otherUserId,
|
||||
msg: client?.getUserId() + " ping",
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
|
||||
|
||||
// fudge this to make it a highlight
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if (ev.getId() === pingEvent.event_id) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
// the room should now have one highlight since our receipt was before the ping message
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
describe("notification processing in threads", () => {
|
||||
let threadEvent1: IRoomEvent;
|
||||
let threadEvent2: IRoomEvent;
|
||||
let firstEventId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
|
||||
|
||||
// Add a threaded event off of the first event
|
||||
threadEvent1 = utils.mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
user: otherUserId,
|
||||
room: roomId,
|
||||
ts: 500,
|
||||
content: {
|
||||
"body": "first thread response",
|
||||
"m.relates_to": {
|
||||
"event_id": firstEventId,
|
||||
"m.in_reply_to": {
|
||||
event_id: firstEventId,
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
},
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(threadEvent1);
|
||||
|
||||
// ...and another
|
||||
threadEvent2 = utils.mkEvent({
|
||||
type: EventType.RoomMessage,
|
||||
user: otherUserId,
|
||||
room: roomId,
|
||||
ts: 1500,
|
||||
content: {
|
||||
"body": "second thread response",
|
||||
"m.relates_to": {
|
||||
"event_id": firstEventId,
|
||||
"m.in_reply_to": {
|
||||
event_id: firstEventId,
|
||||
},
|
||||
"rel_type": "io.element.thread",
|
||||
},
|
||||
},
|
||||
}) as IRoomEvent;
|
||||
syncData.rooms.join[roomId].timeline.events.push(threadEvent2);
|
||||
|
||||
// fudge to make these highlights
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
});
|
||||
|
||||
it("checks threads with notifications on unthreaded receipts", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
// add a receipt for a random, ficticious thread, otherwise the client will
|
||||
// think that the thread is before any threaded receipts and ignore it.
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1, thread_id: "some_other_thread" },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient({ threadSupport: true });
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
// pretend that the client has decrypted an event to trigger it to compute
|
||||
// local notifications
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent1.event_id)!);
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent2.event_id)!);
|
||||
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
|
||||
// we should now have one highlight: the unread message that pings
|
||||
expect(
|
||||
room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight),
|
||||
).toEqual(2);
|
||||
|
||||
const syncData2 = {
|
||||
rooms: {
|
||||
join: {
|
||||
[roomId]: {
|
||||
ephemeral: {
|
||||
events: [
|
||||
{
|
||||
content: {
|
||||
[firstEventId]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData2);
|
||||
|
||||
await Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]);
|
||||
|
||||
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("should recalculate highlights on threaded receipt for encrypted rooms", async () => {
|
||||
const myUserId = client!.getUserId()!;
|
||||
|
||||
// add a receipt for the first message in the threadm leaving the second one unread
|
||||
syncData.rooms.join[roomId].ephemeral.events = [
|
||||
{
|
||||
content: {
|
||||
[threadEvent1.event_id]: {
|
||||
"m.read": {
|
||||
[myUserId]: { ts: 1, thread_id: firstEventId },
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "m.receipt",
|
||||
},
|
||||
];
|
||||
|
||||
// fudge to make both thread replies highlights
|
||||
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
|
||||
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
|
||||
return {
|
||||
notify: true,
|
||||
tweaks: {
|
||||
highlight: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient({ threadSupport: true });
|
||||
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
|
||||
// pretend that the client has decrypted an event to trigger it to compute
|
||||
// local notifications
|
||||
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
|
||||
|
||||
// the room should now have one highlight: the second thread message
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight)).toBe(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1892,6 +2388,57 @@ describe("MatrixClient syncing", () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("msc4222", () => {
|
||||
it("should respect state events in state_after for left rooms", async () => {
|
||||
httpBackend!.when("POST", "/filter").respond(200, {
|
||||
filter_id: "another_id",
|
||||
});
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
leave: {
|
||||
[roomOne]: {
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.topic",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { topic: "A new room topic" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
"state": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { name: "A new room name" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [[room]] = await Promise.all([
|
||||
client!.syncLeftRooms(),
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms make its /sync call
|
||||
httpBackend!.flush("/filter").then(() => {
|
||||
return httpBackend!.flushAllExpected();
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(room.name).toEqual("Empty room");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("peek", () => {
|
||||
@@ -1899,67 +2446,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", () => {
|
||||
@@ -1970,7 +2520,7 @@ describe("MatrixClient syncing", () => {
|
||||
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
|
||||
|
||||
client!.store.storeAccountDataEvents([eventA1, eventB1]);
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
client!.on(ClientEvent.AccountData, fn);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
@@ -2020,16 +2570,15 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
};
|
||||
|
||||
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
|
||||
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, "tests"),
|
||||
});
|
||||
// rust crypto uses by default indexeddb
|
||||
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const idbHttpBackend = idbTestClient.httpBackend;
|
||||
const idbClient = idbTestClient.client;
|
||||
idbHttpBackend.when("GET", "/versions").respond(200, {});
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
await idbClient.initCrypto();
|
||||
await idbClient.initRustCrypto();
|
||||
|
||||
const roomId = "!invite:example.org";
|
||||
|
||||
@@ -2101,7 +2650,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
indexedDB: globalThis.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
@@ -2173,7 +2722,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
indexedDB: globalThis.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -16,14 +16,13 @@ limitations under the License.
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import type HttpBackend from "matrix-mock-request";
|
||||
import {
|
||||
Category,
|
||||
ClientEvent,
|
||||
EventType,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
type ISyncResponse,
|
||||
type MatrixClient,
|
||||
MatrixEvent,
|
||||
NotificationCountType,
|
||||
RelationType,
|
||||
@@ -63,7 +62,7 @@ describe("Notification count fixing", () => {
|
||||
|
||||
client!.startClient({ threadSupport: true });
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: roomId,
|
||||
@@ -78,7 +77,7 @@ describe("Notification count fixing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(event, "getPushActions").mockReturnValue({
|
||||
vi.spyOn(event, "getPushActions").mockReturnValue({
|
||||
notify: true,
|
||||
tweaks: {},
|
||||
});
|
||||
@@ -124,11 +123,11 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
await room.addLiveEvents([thread.rootEvent]);
|
||||
await room.addLiveEvents([thread.rootEvent], { addToState: false });
|
||||
|
||||
// Initialize read receipt datastructure before testing the reaction
|
||||
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
|
||||
@@ -144,7 +143,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
|
||||
let lastEvent: MatrixEvent | null = null;
|
||||
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
vi.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
lastEvent = event as MatrixEvent;
|
||||
return { event_id: reactionEventId };
|
||||
});
|
||||
@@ -152,7 +151,7 @@ describe("MatrixClient syncing", () => {
|
||||
await client!.sendEvent(roomId, EventType.Reaction, {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: threadReply.getId(),
|
||||
event_id: threadReply.getId()!,
|
||||
key: "",
|
||||
},
|
||||
});
|
||||
@@ -196,7 +195,7 @@ describe("MatrixClient syncing", () => {
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import {
|
||||
MSC4108FailureReason,
|
||||
MSC4108RendezvousSession,
|
||||
MSC4108SecureChannel,
|
||||
MSC4108SignInWithQR,
|
||||
PayloadType,
|
||||
RendezvousError,
|
||||
} from "../../../src/rendezvous";
|
||||
import {
|
||||
ClientPrefix,
|
||||
DEVICE_CODE_SCOPE,
|
||||
type IHttpOpts,
|
||||
type IMyDevice,
|
||||
type MatrixClient,
|
||||
MatrixError,
|
||||
MatrixHttpApi,
|
||||
} from "../../../src";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||
const baseUrl = "https://example.com";
|
||||
const crypto = {
|
||||
exportSecretsForQrLogin: vi.fn(),
|
||||
};
|
||||
const client = {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc4108Enabled && feature === "org.matrix.msc4108");
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
baseUrl,
|
||||
getDomain: () => "example.com",
|
||||
getDevice: vi.fn(),
|
||||
getCrypto: vi.fn(() => crypto),
|
||||
getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||
} as unknown as MatrixClient;
|
||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||
baseUrl: client.baseUrl,
|
||||
prefix: ClientPrefix.Unstable,
|
||||
onlyData: true,
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("MSC4108SignInWithQR", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.get("https://issuer/jwks", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
});
|
||||
|
||||
const url = "https://fallbackserver/rz/123";
|
||||
const deviceId = "DEADB33F";
|
||||
const verificationUri = "https://example.com/verify";
|
||||
const verificationUriComplete = "https://example.com/verify/complete";
|
||||
|
||||
it("should generate qr code data as expected", async () => {
|
||||
const session = new MSC4108RendezvousSession({
|
||||
url,
|
||||
});
|
||||
const channel = new MSC4108SecureChannel(session);
|
||||
const login = new MSC4108SignInWithQR(channel, false);
|
||||
|
||||
await login.generateCode();
|
||||
const code = login.code;
|
||||
expect(code).toHaveLength(71);
|
||||
const text = new TextDecoder().decode(code);
|
||||
expect(text.startsWith("MATRIX")).toBeTruthy();
|
||||
expect(text.endsWith(url)).toBeTruthy();
|
||||
|
||||
// Assert that the code is stable
|
||||
await login.generateCode();
|
||||
expect(login.code).toEqual(code);
|
||||
});
|
||||
|
||||
describe("should be able to connect as a reciprocating device", () => {
|
||||
let client: MatrixClient;
|
||||
let ourLogin: MSC4108SignInWithQR;
|
||||
let opponentLogin: MSC4108SignInWithQR;
|
||||
|
||||
beforeEach(async () => {
|
||||
let ourData = Promise.withResolvers<string>();
|
||||
let opponentData = Promise.withResolvers<string>();
|
||||
|
||||
const ourMockSession = {
|
||||
send: vi.fn(async (newData) => {
|
||||
ourData.resolve(newData);
|
||||
}),
|
||||
receive: vi.fn(() => {
|
||||
const prom = opponentData.promise;
|
||||
prom.then(() => {
|
||||
opponentData = Promise.withResolvers();
|
||||
});
|
||||
return prom;
|
||||
}),
|
||||
url,
|
||||
cancelled: false,
|
||||
cancel: () => {
|
||||
// @ts-ignore
|
||||
ourMockSession.cancelled = true;
|
||||
ourData.resolve("");
|
||||
},
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
const opponentMockSession = {
|
||||
send: vi.fn(async (newData) => {
|
||||
opponentData.resolve(newData);
|
||||
}),
|
||||
receive: vi.fn(() => {
|
||||
const prom = ourData.promise;
|
||||
prom.then(() => {
|
||||
ourData = Promise.withResolvers();
|
||||
});
|
||||
return prom;
|
||||
}),
|
||||
url,
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
|
||||
client = makeMockClient({ userId: "@alice:example.com", deviceId: "alice", msc4108Enabled: true });
|
||||
|
||||
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
||||
const qrCodeData = QrCodeData.fromBytes(
|
||||
await ourChannel.generateCode(QrCodeIntent.Reciprocate, client.getDomain()!),
|
||||
);
|
||||
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
|
||||
|
||||
ourLogin = new MSC4108SignInWithQR(ourChannel, true, client);
|
||||
opponentLogin = new MSC4108SignInWithQR(opponentChannel, false);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share server name & check code", async () => {
|
||||
await Promise.all([
|
||||
expect(ourLogin.negotiateProtocols()).resolves.toEqual({}),
|
||||
expect(opponentLogin.negotiateProtocols()).resolves.toEqual({ serverName: client.getDomain() }),
|
||||
]);
|
||||
|
||||
expect(ourLogin.checkCode).toBe(opponentLogin.checkCode);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share verificationUri", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
|
||||
verificationUri: verificationUriComplete,
|
||||
}),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
verification_uri_complete: verificationUriComplete,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort if device already exists", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort on unsupported protocol", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow(
|
||||
"Received a request for an unsupported protocol",
|
||||
),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant_v2",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share secrets", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
|
||||
// Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here
|
||||
// @ts-ignore
|
||||
await opponentLogin.receive();
|
||||
|
||||
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
|
||||
|
||||
const payload = {
|
||||
secrets: expect.objectContaining(secrets),
|
||||
};
|
||||
await Promise.all([
|
||||
expect(ourProm).resolves.toEqual(payload),
|
||||
expect(opponentLogin.shareSecrets()).resolves.toEqual(payload),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
vi.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
// @ts-ignore
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
await expect(ourProm).rejects.toThrow("New device not found");
|
||||
});
|
||||
|
||||
it("should abort on unexpected errors", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
// @ts-ignore
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
vi.mocked(client.getDevice).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
|
||||
);
|
||||
|
||||
await expect(ourLogin.shareSecrets()).rejects.toThrow("The message");
|
||||
});
|
||||
|
||||
it("should abort on declined login", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
await ourLogin.declineLoginOnExistingDevice();
|
||||
await expect(opponentLogin.shareSecrets()).rejects.toThrow(
|
||||
new RendezvousError("Failed", MSC4108FailureReason.UserCancelled),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
const opponentProm = opponentLogin.shareSecrets();
|
||||
|
||||
// Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here
|
||||
// @ts-ignore
|
||||
await opponentLogin.receive();
|
||||
|
||||
const deviceResolvers = Promise.withResolvers<IMyDevice>();
|
||||
vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
|
||||
|
||||
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
|
||||
deviceResolvers.resolve({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourProm).rejects.toThrow("User cancelled"),
|
||||
expect(opponentProm).rejects.toThrow("Unexpected message received"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,56 +15,70 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import { fail } from "assert";
|
||||
|
||||
import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Extension } from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { IRoomEvent, IStateEvent } from "../../src";
|
||||
import type MockHttpBackend from "matrix-mock-request";
|
||||
import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
SlidingSync,
|
||||
SlidingSyncEvent,
|
||||
type MSC3575RoomData,
|
||||
SlidingSyncState,
|
||||
type Extension,
|
||||
} from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
|
||||
import {
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
NotificationCountType,
|
||||
JoinRule,
|
||||
MatrixError,
|
||||
EventType,
|
||||
IPushRules,
|
||||
type IPushRules,
|
||||
PushRuleKind,
|
||||
TweakName,
|
||||
ClientEvent,
|
||||
RoomMemberEvent,
|
||||
RoomEvent,
|
||||
Room,
|
||||
IRoomTimelineData,
|
||||
type Room,
|
||||
type IRoomTimelineData,
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src";
|
||||
import { type SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { type IStoredClientOpts } from "../../src";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
import { type SyncCryptoCallbacks } from "../../src/common-crypto/CryptoBackend";
|
||||
|
||||
declare module "../../src/@types/event" {
|
||||
interface AccountDataEvents {
|
||||
global_test: {};
|
||||
tester: {};
|
||||
}
|
||||
}
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
let sdk: SlidingSyncSdk | undefined;
|
||||
let mockSlidingSync: SlidingSync | undefined;
|
||||
let syncCryptoCallback: SyncCryptoCallbacks | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||
s.getListParams = jest.fn();
|
||||
s.getListData = jest.fn();
|
||||
s.getRoomSubscriptions = jest.fn();
|
||||
s.modifyRoomSubscriptionInfo = jest.fn();
|
||||
s.modifyRoomSubscriptions = jest.fn();
|
||||
s.registerExtension = jest.fn();
|
||||
s.setList = jest.fn();
|
||||
s.setListRanges = jest.fn();
|
||||
s.start = jest.fn();
|
||||
s.stop = jest.fn();
|
||||
s.resend = jest.fn();
|
||||
s.getListParams = vi.fn();
|
||||
s.getListData = vi.fn();
|
||||
s.getRoomSubscriptions = vi.fn();
|
||||
s.modifyRoomSubscriptionInfo = vi.fn();
|
||||
s.modifyRoomSubscriptions = vi.fn();
|
||||
s.registerExtension = vi.fn();
|
||||
s.setList = vi.fn();
|
||||
s.setListRanges = vi.fn();
|
||||
s.start = vi.fn();
|
||||
s.stop = vi.fn();
|
||||
s.resend = vi.fn();
|
||||
return s;
|
||||
};
|
||||
|
||||
@@ -97,7 +111,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(m.getType()).toEqual(want[i].type);
|
||||
expect(m.getSender()).toEqual(want[i].sender);
|
||||
expect(m.getId()).toEqual(want[i].event_id);
|
||||
expect(m.getContent()).toEqual(want[i].content);
|
||||
expect(m.getContent<IContent>()).toEqual(want[i].content);
|
||||
expect(m.getTs()).toEqual(want[i].origin_server_ts);
|
||||
if (want[i].unsigned) {
|
||||
expect(m.getUnsigned()).toEqual(want[i].unsigned);
|
||||
@@ -112,15 +126,16 @@ describe("SlidingSyncSdk", () => {
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
|
||||
testOpts = testOpts || {};
|
||||
const syncOpts: SyncApiOptions = {};
|
||||
const syncOpts: SyncApiOptions = { logger };
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
await client!.initRustCrypto({ useIndexedDB: false });
|
||||
syncCryptoCallback = client!.getCrypto() as unknown as SyncCryptoCallbacks;
|
||||
syncOpts.cryptoCallbacks = syncCryptoCallback;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||
@@ -135,7 +150,7 @@ describe("SlidingSyncSdk", () => {
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension<any, any> => {
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
|
||||
@@ -353,7 +368,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can be created with live events", async () => {
|
||||
const seenLiveEventDeferred = defer<boolean>();
|
||||
const seenLiveEventDeferred = Promise.withResolvers<boolean>();
|
||||
const listener = (
|
||||
ev: MatrixEvent,
|
||||
room?: Room,
|
||||
@@ -601,13 +616,13 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
initial: true,
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
|
||||
],
|
||||
timeline: [],
|
||||
});
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
await emitPromise(client!, RoomMemberEvent.Name);
|
||||
@@ -633,43 +648,38 @@ describe("SlidingSyncSdk", () => {
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
it("gets enabled all the time", async () => {
|
||||
expect(await ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(await ext.onRequest(false)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
client!.crypto!.processDeviceLists = jest.fn();
|
||||
syncCryptoCallback!.processDeviceLists = vi.fn();
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
},
|
||||
});
|
||||
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
|
||||
expect(syncCryptoCallback!.processDeviceLists).toHaveBeenCalledWith({
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
});
|
||||
});
|
||||
|
||||
it("can update OTK counts and unused fallback keys", () => {
|
||||
client!.crypto!.processKeyCounts = jest.fn();
|
||||
syncCryptoCallback!.processKeyCounts = vi.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
expect(syncCryptoCallback!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
"signed_curve25519",
|
||||
]);
|
||||
});
|
||||
@@ -686,11 +696,13 @@ describe("SlidingSyncSdk", () => {
|
||||
ext = findExtension("account_data");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
it("gets enabled all the time", async () => {
|
||||
expect(await ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(await ext.onRequest(false)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes global account data", async () => {
|
||||
@@ -710,7 +722,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeTruthy();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
expect(globalData.getContent<IContent>()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
it("processes rooms account data", async () => {
|
||||
@@ -745,7 +757,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(room).toBeTruthy();
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeTruthy();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
expect(event.getContent<IContent>()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
it("doesn't crash for unknown room account data", async () => {
|
||||
@@ -814,8 +826,12 @@ describe("SlidingSyncSdk", () => {
|
||||
ext = findExtension("to_device");
|
||||
});
|
||||
|
||||
it("gets enabled with a limit on the initial request only", () => {
|
||||
const reqJson: any = ext.onRequest(true);
|
||||
it("gets enabled all the time", async () => {
|
||||
let reqJson: any = await ext.onRequest(true);
|
||||
expect(reqJson.enabled).toEqual(true);
|
||||
expect(reqJson.limit).toBeGreaterThan(0);
|
||||
expect(reqJson.since).toBeUndefined();
|
||||
reqJson = await ext.onRequest(false);
|
||||
expect(reqJson.enabled).toEqual(true);
|
||||
expect(reqJson.limit).toBeGreaterThan(0);
|
||||
expect(reqJson.since).toBeUndefined();
|
||||
@@ -826,11 +842,12 @@ describe("SlidingSyncSdk", () => {
|
||||
next_batch: "12345",
|
||||
events: [],
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual({
|
||||
expect(await ext.onRequest(false)).toMatchObject({
|
||||
since: "12345",
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("can handle missing fields", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "23456",
|
||||
@@ -845,7 +862,7 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
let called = false;
|
||||
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||
expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
|
||||
expect(ev.getType()).toEqual(toDeviceType);
|
||||
called = true;
|
||||
});
|
||||
@@ -910,24 +927,25 @@ describe("SlidingSyncSdk", () => {
|
||||
ext = findExtension("typing");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
it("gets enabled all the time", async () => {
|
||||
expect(await ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(await ext.onRequest(false)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes typing notifications", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
@@ -962,13 +980,12 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
|
||||
initial: true,
|
||||
});
|
||||
const room = client!.getRoom(roomId)!;
|
||||
@@ -1035,11 +1052,13 @@ describe("SlidingSyncSdk", () => {
|
||||
ext = findExtension("receipts");
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
it("gets enabled all the time", async () => {
|
||||
expect(await ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(await ext.onRequest(false)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(ext.onRequest(false)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("processes receipts", async () => {
|
||||
@@ -1077,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("gracefully handles missing rooms when receiving receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
|
||||
+37
-754
@@ -15,21 +15,20 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import EventEmitter from "events";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type EventEmitter from "events";
|
||||
import type MockHttpBackend from "matrix-mock-request";
|
||||
import {
|
||||
SlidingSync,
|
||||
SlidingSyncState,
|
||||
ExtensionState,
|
||||
SlidingSyncEvent,
|
||||
Extension,
|
||||
SlidingSyncEventHandlerMap,
|
||||
MSC3575RoomData,
|
||||
type Extension,
|
||||
type SlidingSyncEventHandlerMap,
|
||||
type MSC3575RoomData,
|
||||
} from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src";
|
||||
import { type MatrixClient } from "../../src";
|
||||
|
||||
/**
|
||||
* Tests for sliding sync. These tests are broken down into sub-tests which are reliant upon one another.
|
||||
@@ -42,7 +41,7 @@ describe("SlidingSync", () => {
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const proxyBaseUrl = "http://localhost:8008";
|
||||
const syncUrl = proxyBaseUrl + "/_matrix/client/unstable/org.matrix.msc3575/sync";
|
||||
const syncUrl = proxyBaseUrl + "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync";
|
||||
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = () => {
|
||||
@@ -83,6 +82,7 @@ describe("SlidingSync", () => {
|
||||
await p;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should stop the sync loop upon calling stop()", () => {
|
||||
slidingSync.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
@@ -104,8 +104,8 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
const ext: Extension<any, any> = {
|
||||
name: () => "custom_extension",
|
||||
onRequest: (initial) => {
|
||||
return { initial: initial };
|
||||
onRequest: async (_) => {
|
||||
return { initial: true };
|
||||
},
|
||||
onResponse: async (res) => {
|
||||
return;
|
||||
@@ -144,18 +144,16 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// expect nothing but ranges and non-initial extensions to be sent
|
||||
// expect all params to be sent TODO: check MSC4186
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
expect(body.lists["a"]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: false });
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams!["pos"]).toEqual("11");
|
||||
})
|
||||
.respond(200, function () {
|
||||
@@ -333,6 +331,7 @@ describe("SlidingSync", () => {
|
||||
await p;
|
||||
});
|
||||
|
||||
// TODO: this does not exist in MSC4186
|
||||
it("should be able to unsubscribe from a room", async () => {
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -390,18 +389,19 @@ describe("SlidingSync", () => {
|
||||
[3, 5],
|
||||
];
|
||||
|
||||
// request first 3 rooms
|
||||
const listReq = {
|
||||
ranges: [[0, 2]],
|
||||
sort: ["by_name"],
|
||||
timeline_limit: 1,
|
||||
required_state: [["m.room.topic", ""]],
|
||||
filters: {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
|
||||
let slidingSync: SlidingSync;
|
||||
it("should be possible to subscribe to a list", async () => {
|
||||
// request first 3 rooms
|
||||
const listReq = {
|
||||
ranges: [[0, 2]],
|
||||
sort: ["by_name"],
|
||||
timeline_limit: 1,
|
||||
required_state: [["m.room.topic", ""]],
|
||||
filters: {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map([["a", listReq]]), {}, client!, 1);
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -453,11 +453,6 @@ describe("SlidingSync", () => {
|
||||
expect(slidingSync.getListData("b")).toBeNull();
|
||||
const syncData = slidingSync.getListData("a")!;
|
||||
expect(syncData.joinedCount).toEqual(500); // from previous test
|
||||
expect(syncData.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
});
|
||||
|
||||
it("should be possible to adjust list ranges", async () => {
|
||||
@@ -468,10 +463,9 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.log("next ranges", body.lists["a"].ranges);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists["a"]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
// list range should be changed
|
||||
listReq.ranges = newRanges;
|
||||
expect(body.lists["a"]).toEqual(listReq); // resend all values TODO: check MSC4186
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
@@ -496,7 +490,9 @@ describe("SlidingSync", () => {
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
// setListRanges for an invalid list key returns an error
|
||||
await expect(slidingSync.setListRanges("idontexist", newRanges)).rejects.toBeTruthy();
|
||||
expect(() => {
|
||||
slidingSync.setListRanges("idontexist", newRanges);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should be possible to add an extra list", async () => {
|
||||
@@ -514,10 +510,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.log("extra list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists["a"]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
expect(body.lists["a"]).toEqual(listReq); // resend all values TODO: check MSC4186
|
||||
expect(body.lists["b"]).toEqual(extraListReq);
|
||||
})
|
||||
.respond(200, {
|
||||
@@ -538,16 +531,6 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("b");
|
||||
expect(joinedCount).toEqual(50);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
@@ -555,706 +538,6 @@ describe("SlidingSync", () => {
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
it("should be possible to get list DELETE/INSERTs", async () => {
|
||||
// move C (2) to A (0)
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 0,
|
||||
room_id: roomC,
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
let listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomA,
|
||||
2: roomB,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
// move C (0) back to A (2)
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 2,
|
||||
room_id: roomC,
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should ignore invalid list indexes", async () => {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 2324324,
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
2: roomC,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should be possible to update a list", async () => {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: {
|
||||
a: {
|
||||
count: 42,
|
||||
ops: [
|
||||
{
|
||||
op: "INVALIDATE",
|
||||
range: [0, 2],
|
||||
},
|
||||
{
|
||||
op: "SYNC",
|
||||
range: [0, 1],
|
||||
room_ids: [roomB, roomC],
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
// update the list with a new filter
|
||||
slidingSync.setList("a", {
|
||||
filters: {
|
||||
is_encrypted: true,
|
||||
},
|
||||
ranges: [[0, 100]],
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(42);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
// this refers to a set of operations where the end result is no change.
|
||||
it("should handle net zero operations correctly", async () => {
|
||||
const indexToRoomId = {
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
};
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 0,
|
||||
room_id: roomA,
|
||||
},
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should handle deletions correctly", async () => {
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: {
|
||||
a: {
|
||||
count: 499,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(499);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should handle insertions correctly", async () => {
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 1,
|
||||
room_id: roomA,
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
let listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomA,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: {
|
||||
a: {
|
||||
count: 501,
|
||||
ops: [
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 1,
|
||||
room_id: roomB,
|
||||
},
|
||||
],
|
||||
},
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(501);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomB,
|
||||
2: roomA,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
// Regression test to make sure things like DELETE 0 INSERT 0 work correctly and we don't
|
||||
// end up losing room IDs.
|
||||
it("should handle insertions with a spurious DELETE correctly", async () => {
|
||||
slidingSync = new SlidingSync(
|
||||
proxyBaseUrl,
|
||||
new Map([
|
||||
[
|
||||
"a",
|
||||
{
|
||||
ranges: [[0, 20]],
|
||||
},
|
||||
],
|
||||
]),
|
||||
{},
|
||||
client!,
|
||||
1,
|
||||
);
|
||||
// initially start with nothing
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "a",
|
||||
lists: {
|
||||
a: {
|
||||
count: 0,
|
||||
ops: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({});
|
||||
|
||||
// insert a room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "b",
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 0,
|
||||
room_id: roomA,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
});
|
||||
|
||||
// insert another room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 0,
|
||||
room_id: roomB,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomA,
|
||||
});
|
||||
|
||||
// insert a final room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
op: "DELETE",
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
op: "INSERT",
|
||||
index: 0,
|
||||
room_id: roomC,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomB,
|
||||
2: roomA,
|
||||
});
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transaction IDs", () => {
|
||||
beforeAll(setupClient);
|
||||
afterAll(teardownClient);
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
// really this applies to them all but it's easier to just test one
|
||||
it("should resolve modifyRoomSubscriptions after SlidingSync.start() is called", async () => {
|
||||
const roomSubInfo = {
|
||||
timeline_limit: 1,
|
||||
required_state: [["m.room.name", ""]],
|
||||
};
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
|
||||
// modification before SlidingSync.start()
|
||||
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomId]).toEqual(roomSubInfo);
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
txnId = body.txn_id;
|
||||
})
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "aaa",
|
||||
txn_id: txnId,
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
name: "foo bar",
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await subscribePromise;
|
||||
});
|
||||
it("should resolve setList during a connection", async () => {
|
||||
const newList = {
|
||||
ranges: [[0, 20]],
|
||||
};
|
||||
const promise = slidingSync.setList("a", newList);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists["a"]).toEqual(newList);
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
txnId = body.txn_id;
|
||||
})
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "bbb",
|
||||
txn_id: txnId,
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should resolve setListRanges during a connection", async () => {
|
||||
const promise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[20, 40]],
|
||||
});
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
txnId = body.txn_id;
|
||||
})
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "ccc",
|
||||
txn_id: txnId,
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should resolve modifyRoomSubscriptionInfo during a connection", async () => {
|
||||
const promise = slidingSync.modifyRoomSubscriptionInfo({
|
||||
timeline_limit: 99,
|
||||
});
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomId]).toEqual({
|
||||
timeline_limit: 99,
|
||||
});
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
txnId = body.txn_id;
|
||||
})
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "ddd",
|
||||
txn_id: txnId,
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
|
||||
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
};
|
||||
const failPromise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
const failPromise2 = slidingSync.setListRanges("a", [[60, 70]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check((req) => {
|
||||
txnId = req.data.txn_id;
|
||||
})
|
||||
.respond(200, () => {
|
||||
// include the txn_id, earlier requests should now be reject()ed.
|
||||
return {
|
||||
pos: "g",
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await Promise.all([
|
||||
expect(failPromise).rejects.toEqual(gotTxnIds[0]),
|
||||
expect(failPromise2).rejects.toEqual(gotTxnIds[1]),
|
||||
httpBackend!.flushAllExpected(),
|
||||
okPromise,
|
||||
]);
|
||||
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
|
||||
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
|
||||
gotTxnIds.push(req.data?.txn_id);
|
||||
};
|
||||
const A = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend!.flushAllExpected();
|
||||
const B = slidingSync.setListRanges("a", [[60, 70]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
|
||||
const C = slidingSync.setListRanges("a", [[0, 20]]);
|
||||
let pendingC = true;
|
||||
C.finally(() => {
|
||||
pendingC = false;
|
||||
});
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(pushTxn)
|
||||
.respond(200, () => {
|
||||
// include the txn_id for B, so C's promise is outstanding
|
||||
return {
|
||||
pos: "C",
|
||||
txn_id: gotTxnIds[1],
|
||||
};
|
||||
});
|
||||
await Promise.all([
|
||||
expect(A).rejects.toEqual(gotTxnIds[0]),
|
||||
httpBackend!.flushAllExpected(),
|
||||
// A is rejected, see above
|
||||
expect(B).resolves.toEqual(gotTxnIds[1]), // B is resolved
|
||||
]);
|
||||
expect(pendingC).toBe(true); // C is pending still
|
||||
});
|
||||
it("should do nothing for unknown txn_ids", async () => {
|
||||
const promise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
let pending = true;
|
||||
promise.finally(() => {
|
||||
pending = false;
|
||||
});
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[20, 40]],
|
||||
});
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
txnId = body.txn_id;
|
||||
})
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "ccc",
|
||||
txn_id: "bogus transaction id",
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(txnId).toBeDefined();
|
||||
expect(pending).toBe(true);
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom room subscriptions", () => {
|
||||
@@ -1544,7 +827,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
const extPre: Extension<any, any> = {
|
||||
name: () => preExtName,
|
||||
onRequest: (initial) => {
|
||||
onRequest: async (initial) => {
|
||||
return onPreExtensionRequest(initial);
|
||||
},
|
||||
onResponse: (res) => {
|
||||
@@ -1554,7 +837,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
const extPost: Extension<any, any> = {
|
||||
name: () => postExtName,
|
||||
onRequest: (initial) => {
|
||||
onRequest: async (initial) => {
|
||||
return onPostExtensionRequest(initial);
|
||||
},
|
||||
onResponse: (res) => {
|
||||
@@ -1569,7 +852,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
const callbackOrder: string[] = [];
|
||||
let extensionOnResponseCalled = false;
|
||||
onPreExtensionRequest = () => {
|
||||
onPreExtensionRequest = async () => {
|
||||
return extReq;
|
||||
};
|
||||
onPreExtensionResponse = async (resp) => {
|
||||
@@ -1609,7 +892,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should be able to send nothing in an extension request/response", async () => {
|
||||
onPreExtensionRequest = () => {
|
||||
onPreExtensionRequest = async () => {
|
||||
return undefined;
|
||||
};
|
||||
let responseCalled = false;
|
||||
@@ -1644,7 +927,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("is possible to register extensions after start() has been called", async () => {
|
||||
slidingSync.registerExtension(extPost);
|
||||
onPostExtensionRequest = () => {
|
||||
onPostExtensionRequest = async () => {
|
||||
return extReq;
|
||||
};
|
||||
let responseCalled = false;
|
||||
|
||||
+15
-5
@@ -14,12 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import DOMException from "domexception";
|
||||
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
|
||||
|
||||
global.DOMException = DOMException as typeof global.DOMException;
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
vi.mock("../src/http-api/utils", async () => ({
|
||||
...(await vi.importActual("../src/http-api/utils")),
|
||||
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
}));
|
||||
|
||||
manageFetchMockGlobally();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.hardReset();
|
||||
fetchMock.mockGlobal();
|
||||
});
|
||||
|
||||
// Don't make test fail too soon due to timeouts while debugging.
|
||||
if (process.env.VSCODE_INSPECTOR_OPTIONS) {
|
||||
vi.setConfig({ testTimeout: 60 * 1000 * 5 }); // 5 minutes
|
||||
}
|
||||
|
||||
@@ -14,83 +14,110 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockOptionsMethodPut } from "fetch-mock";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { ISyncResponder } from "./SyncResponder";
|
||||
import { type ISyncResponder } from "./SyncResponder";
|
||||
|
||||
/**
|
||||
* An object which intercepts `account_data` get and set requests via fetch-mock.
|
||||
* An object which intercepts `account_data` get and set requests via fetch-mock.
|
||||
*
|
||||
* To use this, call {@link interceptSetAccountData} for each type of account date that should be handled. The updated
|
||||
* account data will be stored in {@link accountDataEvents}; it will also trigger a sync response echoing the updated
|
||||
* data.
|
||||
*
|
||||
* Optionally, you can also call {@link interceptGetAccountData}.
|
||||
*/
|
||||
export class AccountDataAccumulator {
|
||||
/**
|
||||
* The account data events to be returned by the sync.
|
||||
* Will be updated when fetchMock intercepts calls to PUT `/_matrix/client/v3/user/:userId/account_data/`.
|
||||
* Will be used by `sendSyncResponseWithUpdatedAccountData`
|
||||
*/
|
||||
public accountDataEvents: Map<String, any> = new Map();
|
||||
public accountDataEvents: Map<string, any> = new Map();
|
||||
|
||||
public constructor(private syncResponder: ISyncResponder) {}
|
||||
|
||||
private accountDataResolvers = new Map<string, PromiseWithResolvers<any>>();
|
||||
private setInterceptRunning = false;
|
||||
|
||||
/**
|
||||
* Intercept requests to set a particular type of account data.
|
||||
* Intercept setting of account data.
|
||||
*
|
||||
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||
* resolved.
|
||||
*
|
||||
* @param accountDataType - type of account data to be intercepted
|
||||
* @param opts - options to pass to fetchMock
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public interceptSetAccountData(accountDataType: string, opts?: MockOptionsMethodPut): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/${accountDataType}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const type = url.split("/").pop();
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
resolve(content);
|
||||
return {};
|
||||
},
|
||||
opts,
|
||||
);
|
||||
public interceptSetAccountData(): void {
|
||||
if (this.setInterceptRunning) return;
|
||||
this.setInterceptRunning = true;
|
||||
|
||||
fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
const type = callLog.url.split("/").pop();
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
|
||||
this.accountDataResolvers.get(type!)?.resolve(content);
|
||||
if (!this.accountDataResolvers.delete(type!)) {
|
||||
// Check for a wildcard matcher
|
||||
for (const [key, resolver] of this.accountDataResolvers) {
|
||||
if (key.endsWith("*") && type?.startsWith(key.slice(0, -1))) {
|
||||
resolver.resolve(content);
|
||||
this.accountDataResolvers.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return a sync response
|
||||
this.sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a particular type of account data.
|
||||
*
|
||||
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||
* resolved.
|
||||
*
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public waitForAccountData(type: string): Promise<any> {
|
||||
const resolvers = Promise.withResolvers<any>();
|
||||
this.accountDataResolvers.set(type, resolvers);
|
||||
this.interceptSetAccountData();
|
||||
return resolvers.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all requests to get account data
|
||||
*/
|
||||
public interceptGetAccountData(): void {
|
||||
fetchMock.get(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
|
||||
(url) => {
|
||||
const type = url.split("/").pop();
|
||||
const existing = this.accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||
const type = callLog.url.split("/").pop();
|
||||
const existing = this.accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a sync response the current account data events.
|
||||
*/
|
||||
public sendSyncResponseWithUpdatedAccountData(syncResponder: ISyncResponder): void {
|
||||
private sendSyncResponseWithUpdatedAccountData(): void {
|
||||
try {
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
this.syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 1,
|
||||
account_data: {
|
||||
events: Array.from(this.accountDataEvents, ([type, content]) => ({
|
||||
@@ -99,7 +126,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.
|
||||
|
||||
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import debugFunc, { type Debugger } from "debug";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
|
||||
import type { CrossSigningKeyInfo } from "../../src/crypto-api";
|
||||
|
||||
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
|
||||
*
|
||||
@@ -55,28 +56,53 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
private readonly debug: Debugger;
|
||||
|
||||
private deviceKeys: IDeviceKeys | null = null;
|
||||
private crossSigningKeys: CrossSigningKeys | null = null;
|
||||
private oneTimeKeys: Record<string, IOneTimeKey> = {};
|
||||
private readonly oneTimeKeysPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* Construct a new E2EKeyReceiver.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
|
||||
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* It will immediately register an intercept of [`/keys/upload`][1], [`/keys/signatures/upload`][2] and
|
||||
* [`/keys/device_signing/upload`][3] requests for the given homeserverUrl.
|
||||
* Only requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
* [1]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keysupload
|
||||
* [2]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keyssignaturesupload
|
||||
* [3]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keysdevice_signingupload
|
||||
*
|
||||
* @param homeserverUrl - The Homeserver Url of the client under test.
|
||||
* @param routeNamePrefix - An optional prefix to add to the fetchmock route names. Required if there is more than
|
||||
* one E2EKeyReceiver instance active.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
public constructor(homeserverUrl: string, routeNamePrefix: string = "") {
|
||||
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
|
||||
|
||||
// set up a listener for /keys/upload.
|
||||
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
||||
const listener = (url: string, options: RequestInit) =>
|
||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
||||
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
|
||||
{ name: routeNamePrefix + "keys-upload" },
|
||||
);
|
||||
});
|
||||
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onSignaturesUploadRequest(callLog.options),
|
||||
{
|
||||
name: routeNamePrefix + "upload-sigs",
|
||||
},
|
||||
);
|
||||
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onSigningKeyUploadRequest(callLog.options),
|
||||
{
|
||||
name: routeNamePrefix + "upload-cross-signing-keys",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
|
||||
@@ -87,8 +113,10 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
if (this.deviceKeys) {
|
||||
throw new Error("Application attempted to upload E2E device keys multiple times");
|
||||
}
|
||||
this.debug(`received device keys`);
|
||||
this.deviceKeys = content.device_keys;
|
||||
this.debug(
|
||||
`received device keys for user ID ${this.deviceKeys!.user_id}, device ID ${this.deviceKeys!.device_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
|
||||
@@ -113,6 +141,47 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
};
|
||||
}
|
||||
|
||||
private async onSignaturesUploadRequest(request: RequestInit): Promise<object> {
|
||||
const content = JSON.parse(request.body as string) as KeySignatures;
|
||||
for (const [userId, userKeys] of Object.entries(content)) {
|
||||
for (const [deviceId, signedKey] of Object.entries(userKeys)) {
|
||||
this.onDeviceSignatureUpload(userId, deviceId, signedKey);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private onDeviceSignatureUpload(userId: string, deviceId: string, signedKey: CrossSigningKeyInfo | ISignedKey) {
|
||||
if (!this.deviceKeys || userId != this.deviceKeys.user_id || deviceId != this.deviceKeys.device_id) {
|
||||
this.debug(
|
||||
`Ignoring device key signature upload for unknown device user ID ${userId}, device ID ${deviceId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug(`received device key signature for user ID ${userId}, device ID ${deviceId}`);
|
||||
this.deviceKeys.signatures ??= {};
|
||||
for (const [signingUser, signatures] of Object.entries(signedKey.signatures!)) {
|
||||
this.deviceKeys.signatures[signingUser] = Object.assign(
|
||||
this.deviceKeys.signatures[signingUser] ?? {},
|
||||
signatures,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async onSigningKeyUploadRequest(request: RequestInit): Promise<object> {
|
||||
const content = JSON.parse(request.body as string);
|
||||
if (this.crossSigningKeys) {
|
||||
throw new Error("Application attempted to upload E2E cross-signing keys multiple times");
|
||||
}
|
||||
this.debug(`received cross-signing keys`);
|
||||
// Remove UIA data
|
||||
delete content["auth"];
|
||||
this.crossSigningKeys = content;
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Get the uploaded Ed25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
@@ -150,6 +219,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
return this.deviceKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If cross-signing keys have been uploaded, return them. Else return null.
|
||||
*/
|
||||
public getUploadedCrossSigningKeys(): CrossSigningKeys | null {
|
||||
return this.crossSigningKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
@@ -161,4 +237,18 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
await this.oneTimeKeysPromise;
|
||||
return this.oneTimeKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If no one-time keys have yet been uploaded, return `null`.
|
||||
* Otherwise, pop a key from the uploaded list.
|
||||
*/
|
||||
public getOneTimeKey(): [string, IOneTimeKey] | null {
|
||||
const keys = Object.entries(this.oneTimeKeys);
|
||||
if (keys.length == 0) {
|
||||
return null;
|
||||
}
|
||||
const [otkId, otk] = keys[0];
|
||||
delete this.oneTimeKeys[otkId];
|
||||
return [otkId, otk];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { IDownloadKeyResult } from "../../src";
|
||||
import { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
|
||||
import { type IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
|
||||
/**
|
||||
* An object which intercepts `/keys/query` fetches via fetch-mock.
|
||||
@@ -42,26 +42,23 @@ export class E2EKeyResponder {
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
// set up a listener for /keys/query.
|
||||
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), (callLog) =>
|
||||
this.onKeyQueryRequest(callLog.options),
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyQueryRequest(options: RequestInit) {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const usersToReturn = Object.keys(content["device_keys"]);
|
||||
const response = {
|
||||
device_keys: {} as { [userId: string]: any },
|
||||
master_keys: {} as { [userId: string]: any },
|
||||
self_signing_keys: {} as { [userId: string]: any },
|
||||
user_signing_keys: {} as { [userId: string]: any },
|
||||
failures: {} as { [serverName: string]: any },
|
||||
};
|
||||
device_keys: {},
|
||||
master_keys: {},
|
||||
self_signing_keys: {},
|
||||
user_signing_keys: {},
|
||||
failures: {},
|
||||
} as IDownloadKeyResult;
|
||||
for (const user of usersToReturn) {
|
||||
const userKeys = this.deviceKeysByUserByDevice.get(user);
|
||||
if (userKeys !== undefined) {
|
||||
response.device_keys[user] = Object.fromEntries(userKeys.entries());
|
||||
}
|
||||
|
||||
// First see if we have an E2EKeyReceiver for this user, and if so, return any keys that have been uploaded
|
||||
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
|
||||
if (e2eKeyReceiver !== undefined) {
|
||||
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
|
||||
@@ -69,16 +66,27 @@ export class E2EKeyResponder {
|
||||
response.device_keys[user] ??= {};
|
||||
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
|
||||
}
|
||||
const crossSigningKeys = e2eKeyReceiver.getUploadedCrossSigningKeys();
|
||||
if (crossSigningKeys !== null) {
|
||||
response.master_keys![user] = crossSigningKeys["master_key"];
|
||||
response.self_signing_keys![user] = crossSigningKeys["self_signing_key"] as SigningKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// Mix in any keys that have been added explicitly to this E2EKeyResponder.
|
||||
const userKeys = this.deviceKeysByUserByDevice.get(user);
|
||||
if (userKeys !== undefined) {
|
||||
response.device_keys[user] ??= {};
|
||||
Object.assign(response.device_keys[user], Object.fromEntries(userKeys.entries()));
|
||||
}
|
||||
if (this.masterKeysByUser.hasOwnProperty(user)) {
|
||||
response.master_keys[user] = this.masterKeysByUser[user];
|
||||
response.master_keys![user] = this.masterKeysByUser[user];
|
||||
}
|
||||
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
|
||||
response.self_signing_keys![user] = this.selfSigningKeysByUser[user];
|
||||
}
|
||||
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
|
||||
response.user_signing_keys![user] = this.userSigningKeysByUser[user];
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
import { type IClaimKeysRequest } from "../../src";
|
||||
|
||||
/**
|
||||
* An object which intercepts `/keys/claim` fetches via fetch-mock.
|
||||
*/
|
||||
export class E2EOTKClaimResponder {
|
||||
private e2eKeyReceiversByUserByDevice = new MapWithDefault<string, Map<string, E2EKeyReceiver>>(() => new Map());
|
||||
|
||||
/**
|
||||
* Construct a new E2EOTKClaimResponder.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/claim` requests for the given homeserverUrl.
|
||||
* Only /claim requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
|
||||
this.onKeyClaimRequest(callLog.options),
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyClaimRequest(options: RequestInit) {
|
||||
const content = JSON.parse(options.body as string) as IClaimKeysRequest;
|
||||
const response = {
|
||||
one_time_keys: {} as { [userId: string]: any },
|
||||
};
|
||||
for (const [userId, devices] of Object.entries(content["one_time_keys"])) {
|
||||
for (const deviceId of Object.keys(devices)) {
|
||||
const e2eKeyReceiver = this.e2eKeyReceiversByUserByDevice.get(userId)?.get(deviceId);
|
||||
const otk = e2eKeyReceiver?.getOneTimeKey();
|
||||
if (otk) {
|
||||
const [keyId, key] = otk;
|
||||
response.one_time_keys[userId] ??= {};
|
||||
response.one_time_keys[userId][deviceId] = {
|
||||
[keyId]: key,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an E2EKeyReceiver to poll for uploaded keys
|
||||
*
|
||||
* When the `/keys/claim` request is received, a OTK will be removed from the `E2EKeyReceiver` and
|
||||
* added to the response.
|
||||
*/
|
||||
public addKeyReceiver(userId: string, deviceId: string, e2eKeyReceiver: E2EKeyReceiver) {
|
||||
this.e2eKeyReceiversByUserByDevice.getOrCreate(userId).set(deviceId, e2eKeyReceiver);
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import { type Debugger } from "debug";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
@@ -75,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (callLog) =>
|
||||
this.onSyncRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<MockResponse> {
|
||||
private async onSyncRequest(): Promise<RouteResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { MatrixEvent } from "../../src";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { LocationAssetType } from "../../src/@types/location";
|
||||
import { type LocationAssetType } from "../../src/@types/location";
|
||||
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
|
||||
type InfoContentProps = {
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+24
-18
@@ -14,12 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
|
||||
import { type MockedObject } from "vitest";
|
||||
|
||||
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
|
||||
import { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { User } from "../../src/models/user";
|
||||
|
||||
// Cribbed from https://github.com/jestjs/jest/blob/94830794dc5dfca1b49bc435b7b031b27838a798/packages/jest-mock/src/index.ts
|
||||
type FunctionLike = (...args: any) => any;
|
||||
type MethodLikeKeys<T> = keyof {
|
||||
[K in keyof T as Required<T>[K] extends FunctionLike ? K : never]: T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock client with real event emitter
|
||||
* useful for testing code that listens
|
||||
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
|
||||
|
||||
/**
|
||||
* - make a mock client
|
||||
* - cast the type to mocked(MatrixClient)
|
||||
* - cast the type to vi.mocked(MatrixClient)
|
||||
* - spy on MatrixClientPeg.get to return the mock
|
||||
* eg
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
getUserId: vi.fn().mockReturnValue(aliceId),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const getMockClientWithEventEmitter = (
|
||||
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
|
||||
): MockedObject<MatrixClient> => {
|
||||
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
const mock = vi.mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
return mock;
|
||||
};
|
||||
|
||||
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
getUserId: vi.fn().mockReturnValue(userId),
|
||||
getSafeUserId: vi.fn().mockReturnValue(userId),
|
||||
getUser: vi.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: vi.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
credentials: { userId },
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: jest.fn(),
|
||||
getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: vi.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsEvents = () => ({
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
decryptEventIfNeeded: vi.fn(),
|
||||
getPushActionsForEvent: vi.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCapabilities: jest.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
getIdentityServerUrl: vi.fn(),
|
||||
getHomeserverUrl: vi.fn(),
|
||||
getCachedCapabilities: vi.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MockInstance } from "vitest";
|
||||
|
||||
/**
|
||||
* Filter emitter.emit mock calls to find relevant events
|
||||
* eg:
|
||||
* ```
|
||||
* const emitSpy = jest.spyOn(state, 'emit');
|
||||
* const emitSpy = vi.spyOn(state, 'emit');
|
||||
* << actions >>
|
||||
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
|
||||
* expect(beaconLivenessEmits.length).toBe(1);
|
||||
* ```
|
||||
*/
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: MockInstance<(...args: any[]) => any>) =>
|
||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||
|
||||
@@ -14,12 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
// Vitest lacks tickAsync() and a number of other async methods which break the event loop,
|
||||
// letting scheduled promise callbacks run. So we have to do it manually
|
||||
// (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
|
||||
// hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise((r) => {
|
||||
|
||||
@@ -14,39 +14,33 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { KeyBackupInfo } from "../../src/crypto-api";
|
||||
import { type KeyBackupInfo } from "../../src/crypto-api";
|
||||
|
||||
/**
|
||||
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
|
||||
*
|
||||
* @param homeserverUrl - the homeserver url for the client under test
|
||||
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
||||
{ versions: ["v1.1"] },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
|
||||
{},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(),
|
||||
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter/fid`, homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the requests needed to set up cross signing
|
||||
* Mock the requests needed to set up cross signing, besides those provided by {@link E2EKeyReceiver}.
|
||||
*
|
||||
* Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request
|
||||
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
|
||||
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
|
||||
*/
|
||||
export function mockSetupCrossSigningRequests(): void {
|
||||
// have account_data requests return an empty object
|
||||
@@ -54,19 +48,6 @@ export function mockSetupCrossSigningRequests(): void {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,24 +60,30 @@ export function mockSetupCrossSigningRequests(): void {
|
||||
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
|
||||
*/
|
||||
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
},
|
||||
},
|
||||
});
|
||||
{ name: "room-keys-version" },
|
||||
);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
});
|
||||
fetchMock.post(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
(callLog) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.modifyRoute("room-keys-version", { response: backupData });
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
},
|
||||
{ name: "post-room-keys-version" },
|
||||
);
|
||||
}
|
||||
|
||||
+1
-35
@@ -14,38 +14,4 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
|
||||
|
||||
/**
|
||||
* Makes a valid OidcClientConfig with minimum valid values
|
||||
* @param issuer used as the base for all other urls
|
||||
* @returns OidcClientConfig
|
||||
*/
|
||||
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
|
||||
const metadata = mockOpenIdConfiguration(issuer);
|
||||
|
||||
return {
|
||||
accountManagementEndpoint: issuer + "account",
|
||||
registrationEndpoint: metadata.registration_endpoint,
|
||||
authorizationEndpoint: metadata.authorization_endpoint,
|
||||
tokenEndpoint: metadata.token_endpoint,
|
||||
metadata,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Useful for mocking <issuer>/.well-known/openid-configuration
|
||||
* @param issuer used as the base for all other urls
|
||||
* @returns ValidatedIssuerMetadata
|
||||
*/
|
||||
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
|
||||
issuer,
|
||||
revocation_endpoint: issuer + "revoke",
|
||||
token_endpoint: issuer + "token",
|
||||
authorization_endpoint: issuer + "auth",
|
||||
registration_endpoint: issuer + "registration",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
});
|
||||
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -83,9 +84,9 @@ def main() -> None:
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
|
||||
import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -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,21 +240,12 @@ 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` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
||||
json.dumps(set_of_exported_room_keys, indent=4)
|
||||
@@ -277,14 +269,39 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
|
||||
|
||||
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
|
||||
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const {prefix}PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {{
|
||||
[{prefix}MEGOLM_SESSION_DATA.session_id]: {prefix}CURVE25519_KEY_BACKUP_DATA
|
||||
}};
|
||||
"""
|
||||
|
||||
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
|
||||
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)
|
||||
)
|
||||
@@ -376,7 +393,7 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||
def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
|
||||
"""
|
||||
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
|
||||
that can be imported via importRoomKeys API.
|
||||
that can be imported via importRoomKeys API, or shared via MSC4268 room history sharing.
|
||||
Returns the exported key, the matching privat edKey (needed to encrypt)
|
||||
"""
|
||||
index = 0
|
||||
@@ -400,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
|
||||
"session_id": encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
),
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||
},
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"m.shared_history": True,
|
||||
}
|
||||
|
||||
return megolm_export, private_key
|
||||
@@ -449,7 +467,7 @@ def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed2
|
||||
"room_id": "!room:id",
|
||||
"sender_key": previous["sender_key"],
|
||||
"session_id": previous["session_id"],
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": previous["sender_claimed_keys"],
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
}
|
||||
@@ -600,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
|
||||
|
||||
message += signature
|
||||
|
||||
cipher_text = encode_base64(message)
|
||||
cipher_text = encode_base64(bytes(message))
|
||||
|
||||
encrypted_payload = {
|
||||
"algorithm" : "m.megolm.v1.aes-sha2",
|
||||
@@ -644,7 +662,7 @@ def export_recovery_key(key_b64: str) -> str:
|
||||
export_bytes += parity_byte.to_bytes(1, 'big')
|
||||
|
||||
# The byte string is encoded using base58
|
||||
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
|
||||
recovery_key = base58.b58encode(bytes(export_bytes)).decode('utf-8')
|
||||
|
||||
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
|
||||
return ' '.join(split)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { IDownloadKeyResult, IEvent } from "../../../src";
|
||||
import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
import type { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import type { IDownloadKeyResult, IEvent } from "../../../src";
|
||||
import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -118,26 +118,6 @@ export const ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -174,7 +156,8 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of MEGOLM_SESSION_DATA */
|
||||
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d/r0NTff1SQt+1GopZkT0nq6jF5Wh/oX+8iwtYjHvTxMpN1UQoXAvRF40O+EVg+Q3efJXh1t45cMco8EWU64VerOir+k7cQ3C9FtcgQw3kmz3s3HeVY10o13X/w6+rc8n6vXqxuIxYHnFxanxX8B6TgTMZNajNfVsmJV0aC1aezim7E2gsftc+6+zW5G+rCFaEsWV/IuSOUz0+Hh0U+7hzSrz9/4qXPEVmPy1f6Ll4hhquPAlXPVDwddqlJDYj7kmvzr1g3bKVpk+TtKDbWlVQDPaJx2DEI2jGkPYjhYb7okpTFKpUny94dZmFIQqCeSGPIniaq8Y+/CanugQ1ZRVQcThuXrTewqWhXcpVvkVHT9i4ImcpBl95HzCBXuiwSUv6FKvO25fp++w555rbn2piFtilrUwnkrZPW32jFuaQcKZF4mZwcLeH7POL5UCuS4TWyaKyArp7bRzXwWuIq1wPET2nAMUmUVL7ge2+tAevk1WOIsjLgSaz/g55wO3Yma7yhXRFKcnzTjS0hUQOZ3GfTNwCM4pjzAtIPzvVd4Fp0b1emWZS5WyOYdXsceEDi3c6WtkoHWOKhPU0zBzn8hA9TdlFFqKzf2QFbN5Zgg0gprDLnLWgpc3/ieI4C7ndEQ7ZeTNMXbT/Y10APFk3qO+IGkLXJ97/qTF41EXFDhlsL0",
|
||||
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
|
||||
"mac": "OibmACbORhI"
|
||||
}
|
||||
@@ -229,6 +212,37 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key that matches the public key in CURVE25519_KEY_BACKUP_DATA */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** base64-encoded backup decryption (private) key that does not match the public key in CURVE25519_KEY_BACKUP_DATA */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64_ALT = "dh4fP2LITyJusgnb0dEq/SQK253WGObvLxXF5FEX6qc";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[MEGOLM_SESSION_DATA.session_id]: CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
// Bob data
|
||||
|
||||
export const BOB_TEST_USER_ID = "@bob:xyz";
|
||||
@@ -338,26 +352,6 @@ export const BOB_ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -369,7 +363,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -380,7 +375,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -394,7 +390,8 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
|
||||
@@ -416,7 +413,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAA7TEpHlx8Ks23hCqXmVW710VjqK2K9xnWCyJvkHfE8x0w6AYvffDj+tRVP8C8M7t4849rD2itn0uma+YMkvjG/nANUTxG1dBf3oUOZ673vflCPoaz7s7x9ZNhYDVSVH5JTdMgNwwN42R5dqqxnGTu516tJzJh/9BWvyD9oIPWJ8X0rt1sbzEJ3PZeBXcSy8GTlZ1SgSFjeiXlwYxOZCaX2sxprk4N1oI1db6g+wCDBhbCGGucJIlTDJna/h9/C5J4drGd/fkisG3SidUmJXXCyInhs/BhwjGAtTGeQS8j7R8UnJxhMulYBHSckzj0Kas71LElPp8W8M4Jq81APA03n5UfYB+U6jbxjDgf8OJnxGQyrteq9F2+SEvS/TwHe1pE3t6EM2mDYRoYDTpU5pTNYSJkGIQMfWJKRxxuWUGs29o1twewJ6dhHgm+SlCII0M7ESoVdV54vxZCvHZnPcR0NXDzal7ils7zBKJmamHfPQBuaqNPU3KmSo+5R8ngFPaWU5LbWqYp/WxSBfNCoLZ7Jf8Io5uitjXTATR2qy2r6l/RJmk3RlfP51kliQqI2TWqRF96oaB96IGgUGSFCX/2pv0psOBGc1SjfmMB3d7gYis+2iBYVbG3xmnpeXbqvlD0Lw9TiTIPkjhJkTW1+lXyhy1xVH9ZmcFamcL7bX15Jx",
|
||||
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
|
||||
"mac": "lEfHlqfJQwU"
|
||||
}
|
||||
@@ -449,3 +446,78 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from BOB_MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const BOB_PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[BOB_MEGOLM_SESSION_DATA.session_id]: BOB_CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+164
-41
@@ -1,25 +1,32 @@
|
||||
import mkdebug from "debug";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import EventEmitter from "events";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "../olm-loader";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import type EventEmitter from "events";
|
||||
import {
|
||||
type IContent,
|
||||
type IEvent,
|
||||
type IEventRelation,
|
||||
type IUnsigned,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../src/models/event";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventType,
|
||||
IJoinedRoom,
|
||||
IPusher,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
HistoryVisibility,
|
||||
type IJoinedRoom,
|
||||
type IPusher,
|
||||
type ISyncResponse,
|
||||
type MatrixClient,
|
||||
MsgType,
|
||||
RelationType,
|
||||
} from "../../src";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { TEST_ROOM_ID } from "./test-data";
|
||||
import { KnownMembership, Membership } from "../../src/@types/membership";
|
||||
import { KnownMembership, type Membership } from "../../src/@types/membership";
|
||||
|
||||
const debug = mkdebug("test-utils");
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -35,7 +42,7 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
|
||||
const p = new Promise<void>((resolve) => {
|
||||
const cb = (state: SyncState) => {
|
||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
debug(`syncPromise(${count}): ${state}`);
|
||||
if (state === SyncState.Syncing) {
|
||||
resolve();
|
||||
} else {
|
||||
@@ -51,13 +58,22 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
|
||||
* @param roomMembers
|
||||
* @param roomId
|
||||
* Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
|
||||
* and history visibility set to `shared`.
|
||||
*
|
||||
* @returns the sync response
|
||||
* @param roomMembers - An array of user IDs representing the members of the room.
|
||||
* @param roomHistoryVisibility - The history visibility setting for the room. Defaults to `shared`.
|
||||
* @param roomId - The ID of the room. Defaults to `TEST_ROOM_ID`.
|
||||
* @param encryptStateEvents - A boolean indicating whether state events should be encrypted. Defaults to `false`.
|
||||
*
|
||||
* @returns The sync response object containing the room data.
|
||||
*/
|
||||
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse {
|
||||
export function getSyncResponse(
|
||||
roomMembers: string[],
|
||||
roomHistoryVisibility: HistoryVisibility = HistoryVisibility.Shared,
|
||||
roomId = TEST_ROOM_ID,
|
||||
encryptStateEvents = false,
|
||||
): ISyncResponse {
|
||||
const roomResponse: IJoinedRoom = {
|
||||
summary: {
|
||||
"m.heroes": [],
|
||||
@@ -71,7 +87,16 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
|
||||
},
|
||||
}),
|
||||
mkEventCustom({
|
||||
sender: roomMembers[0],
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: roomHistoryVisibility,
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -86,7 +111,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
};
|
||||
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
roomResponse.state!.events.push(
|
||||
mkMembershipCustom({
|
||||
membership: KnownMembership.Join,
|
||||
sender: roomMembers[i],
|
||||
@@ -125,9 +150,9 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
// eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
result[key] = vi.fn();
|
||||
}
|
||||
} catch (ex) {
|
||||
} catch {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
// cause exceptions.
|
||||
// Overwriting will not work either in that case.
|
||||
@@ -173,8 +198,10 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
prev_content: opts.prev_content,
|
||||
unsigned: opts.unsigned || {},
|
||||
unsigned: {
|
||||
...opts.unsigned,
|
||||
prev_content: opts.prev_content,
|
||||
},
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
redacts: opts.redacts,
|
||||
@@ -514,25 +541,25 @@ export async function awaitDecryption(
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
|
||||
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
|
||||
debug(`event ${event.getId()} got decryption error; waiting`);
|
||||
} else {
|
||||
return event;
|
||||
}
|
||||
} else {
|
||||
logger.log(`${Date.now()}: event ${event.getId()} is not yet decrypted; waiting`);
|
||||
debug(`event ${event.getId()} is not yet decrypted; waiting`);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (waitOnDecryptionFailure) {
|
||||
event.on(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
debug(`MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
if (!err) {
|
||||
resolve(ev);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
debug(`MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
resolve(ev);
|
||||
});
|
||||
}
|
||||
@@ -550,20 +577,21 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
...extra,
|
||||
});
|
||||
|
||||
/**
|
||||
* a list of the supported crypto implementations, each with a callback to initialise that implementation
|
||||
* for the given client
|
||||
*/
|
||||
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
|
||||
export type InitCrypto = (_: MatrixClient) => Promise<void>;
|
||||
|
||||
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
|
||||
if (global.Olm) {
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
|
||||
}
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
|
||||
|
||||
/**
|
||||
* Counts the number of times that an event was emitted.
|
||||
*/
|
||||
export class EventCounter {
|
||||
public counter;
|
||||
constructor(emitter: EventEmitter, event: string) {
|
||||
this.counter = 0;
|
||||
emitter.on(event, () => {
|
||||
this.counter++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the fake timers in a loop until the given promise resolves or rejects.
|
||||
*
|
||||
@@ -578,8 +606,103 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
|
||||
});
|
||||
|
||||
while (!resolved) {
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
}
|
||||
|
||||
return await promise;
|
||||
}
|
||||
|
||||
export function jestFakeTimersAreEnabled(): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(setTimeout, "clock");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `callback` in a loop, until it returns a successful result (i.e. it does not throw), or we reach a timeout
|
||||
*
|
||||
* Based on the function of the same name in the {@link https://testing-library.com/docs/dom-testing-library/api-async/#waitfor DOM testing library}.
|
||||
*
|
||||
* @param callback - The function to call to check if we can proceed. If it returns a result (including a falsey one),
|
||||
* `waitFor` returns that result. If it throws, `waitFor` continues to wait.
|
||||
*
|
||||
* May return a promise, in which case no further checks are done until the promise resolves.
|
||||
*
|
||||
* @param timeout - The time to wait for, overall, in ms. If `callback` still hasn't returned a successful result after
|
||||
* this time, `waitFor` will throw an error.
|
||||
*
|
||||
* Defaults to 1000.
|
||||
*
|
||||
* @param interval - How often to call `callback`. Defaults to 50.
|
||||
*/
|
||||
export function waitFor<T>(
|
||||
callback: () => Promise<T> | T,
|
||||
{
|
||||
timeout = 1000,
|
||||
interval = 50,
|
||||
}: {
|
||||
timeout?: number;
|
||||
interval?: number;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let lastError: any;
|
||||
let finished = false;
|
||||
let intervalId: ReturnType<typeof setTimeout> | undefined;
|
||||
let promisePending = false;
|
||||
|
||||
const overallTimeoutTimer = setTimeout(handleTimeout, timeout);
|
||||
const usingJestFakeTimers = jestFakeTimersAreEnabled();
|
||||
if (usingJestFakeTimers) {
|
||||
checkCallback();
|
||||
|
||||
while (!finished) {
|
||||
vi.advanceTimersByTime(interval);
|
||||
|
||||
// Could have timed-out
|
||||
if (finished) break;
|
||||
|
||||
checkCallback();
|
||||
}
|
||||
} else {
|
||||
intervalId = setInterval(checkCallback, interval);
|
||||
checkCallback();
|
||||
}
|
||||
|
||||
function checkCallback() {
|
||||
if (promisePending) {
|
||||
// still waiting for the previous check
|
||||
return;
|
||||
}
|
||||
|
||||
async function doCheck() {
|
||||
try {
|
||||
const result = await callback();
|
||||
onDone();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
// Save the most recent callback error to reject the promise with it in the event of a timeout
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
promisePending = true;
|
||||
doCheck().finally(() => {
|
||||
promisePending = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onDone(): void {
|
||||
finished = true;
|
||||
clearTimeout(overallTimeoutTimer);
|
||||
if (intervalId !== undefined) clearInterval(intervalId);
|
||||
}
|
||||
|
||||
function handleTimeout() {
|
||||
onDone();
|
||||
if (lastError) {
|
||||
reject(lastError);
|
||||
} else {
|
||||
reject(new Error("Timed out in waitFor."));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## Dump of an empty libolm indexeddb cryptostore to test skipping migration
|
||||
|
||||
A dump of an account which is almost completely empty, and totally unsuitable
|
||||
for use as a real account.
|
||||
|
||||
This dump was manually created by copying and editing full_account.
|
||||
|
||||
Created to test
|
||||
["Unable to restore session" error due due to half-initialised legacy indexeddb crypto store #27447](https://github.com/element-hq/element-web/issues/27447).
|
||||
We should not launch the Rust migration code when we find a DB in this state.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"account": [],
|
||||
"device_data": [],
|
||||
"inbound_group_sessions": [],
|
||||
"inbound_group_sessions_withheld": [],
|
||||
"notified_error_devices": [],
|
||||
"outgoingRoomKeyRequests": [],
|
||||
"parked_shared_history": [],
|
||||
"rooms": [],
|
||||
"session_problems": [],
|
||||
"sessions": [],
|
||||
"sessions_needing_backup": [],
|
||||
"shared_history_inbound_group_sessions": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user