Compare commits
539 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f9e68c8f3 | |||
| 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 | |||
| fd3a4d4403 | |||
| 93d96281fd | |||
| 944dc51c58 | |||
| c6b43dd176 | |||
| f03dd7b7bc | |||
| 51fa1866a9 | |||
| d76fb2baa0 | |||
| 3feafc9c17 | |||
| c9075b3dba | |||
| 69c474dda7 | |||
| 73ce51065f | |||
| 5d0407d0a6 |
@@ -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,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
@@ -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",
|
||||
{
|
||||
@@ -66,12 +86,11 @@ 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"],
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -92,10 +111,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,6 +134,15 @@ 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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -120,6 +151,18 @@ module.exports = {
|
||||
// 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",
|
||||
},
|
||||
},
|
||||
{
|
||||
// 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -2,11 +2,13 @@
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
/scripts/** @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
/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@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
|
||||
@@ -29,13 +29,13 @@ runs:
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
- name: "A-Element-R"
|
||||
description: "Issues affecting the port of Element's crypto layer to Rust"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Packaging"
|
||||
description: "Packaging, signing, releasing"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Technical-Debt"
|
||||
color: "bfd4f2"
|
||||
- name: "A-Testing"
|
||||
description: "Testing, code coverage, etc."
|
||||
color: "bfd4f2"
|
||||
- name: "backport staging"
|
||||
description: "Label to automatically backport PR to staging branch"
|
||||
color: "B60205"
|
||||
- name: "Dependencies"
|
||||
description: "Pull requests that update a dependency file"
|
||||
color: "0366d6"
|
||||
- name: "Easy"
|
||||
color: "5dc9f7"
|
||||
- name: "Sponsored"
|
||||
color: "ffc8f4"
|
||||
- name: "T-Deprecation"
|
||||
description: "A pull request that makes something deprecated"
|
||||
color: "98e6ae"
|
||||
- name: "T-Other"
|
||||
description: "Questions, user support, anything else"
|
||||
color: "98e6ae"
|
||||
- name: "X-Blocked"
|
||||
color: "ff7979"
|
||||
- name: "X-Breaking-Change"
|
||||
color: "ff7979"
|
||||
- name: "X-Reverted"
|
||||
description: "PR has been reverted"
|
||||
color: "F68AA3"
|
||||
- name: "X-Upcoming-Release-Blocker"
|
||||
description: "This does not affect the current release cycle but will affect the next one"
|
||||
color: "e99695"
|
||||
- name: "Z-Community-PR"
|
||||
description: "Issue is solved by a community member's PR"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
@@ -7,10 +7,12 @@ on:
|
||||
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: >
|
||||
|
||||
@@ -5,16 +5,19 @@ on:
|
||||
workflows: ["Static Analysis"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
netlify:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
|
||||
permissions: {} # No permissions required
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
@@ -21,14 +21,13 @@ concurrency:
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
|
||||
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
# from creeping in. They take a long time to run and consume multiple concurrent runners.
|
||||
skip: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
@@ -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,12 +13,10 @@ 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
|
||||
|
||||
@@ -9,12 +9,13 @@ on:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@132879b972cb7f2ac593006455875098e73cc7f2 # v5
|
||||
- uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
@@ -29,7 +30,7 @@ jobs:
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
@@ -42,8 +43,10 @@ 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]'
|
||||
@@ -69,7 +72,7 @@ jobs:
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Release Sanity checks
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: false
|
||||
inputs:
|
||||
repository:
|
||||
type: string
|
||||
required: false
|
||||
default: ${{ github.repository }}
|
||||
description: "The repository (in form owner/repo) to check for release blockers"
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
REPO: ${{ inputs.repository }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { REPO } = process.env;
|
||||
const { data } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${REPO} label:X-Release-Blocker is:open`,
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
if (data.total_count) {
|
||||
data.items.forEach(item => {
|
||||
core.error(`Release blocker: ${item.html_url}`);
|
||||
});
|
||||
core.setFailed(`Found release blockers!`);
|
||||
}
|
||||
@@ -8,9 +8,12 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
concurrency: release-drafter-action
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -56,7 +59,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("/")) {
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches: [staging]
|
||||
workflow_dispatch: {}
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
draft:
|
||||
permissions:
|
||||
contents: write
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
||||
|
||||
@@ -13,12 +13,14 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -34,6 +36,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -48,9 +51,6 @@ 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
|
||||
run: |
|
||||
@@ -60,26 +60,25 @@ jobs:
|
||||
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
|
||||
echo "Current $PACKAGE version is $CURRENT_VERSION"
|
||||
|
||||
if [ "$CURRENT_VERSION" == "null" ]
|
||||
if [[ "$CURRENT_VERSION" == "null" ]]
|
||||
then
|
||||
echo "Unable to find $PACKAGE in package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_VERSION" == "develop" ]
|
||||
if [[ "$CURRENT_VERSION" == *"#develop" ]]
|
||||
then
|
||||
echo "Not updating dependency $PACKAGE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Resetting $1 to develop branch..."
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $PACKAGE back to develop branch"
|
||||
done <<< "$DEPENDENCIES"
|
||||
env:
|
||||
DEPENDENCIES: ${{ inputs.dependencies }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
|
||||
- name: Push changes
|
||||
run: git push origin develop
|
||||
|
||||
@@ -20,14 +20,6 @@ on:
|
||||
description: Publish to npm
|
||||
type: boolean
|
||||
default: false
|
||||
downstreams:
|
||||
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
|
||||
type: string
|
||||
required: false
|
||||
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
|
||||
@@ -42,16 +34,31 @@ on:
|
||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||
type: number
|
||||
required: false
|
||||
outputs:
|
||||
npm-id:
|
||||
description: "The npm package@version string we published"
|
||||
value: ${{ jobs.npm.outputs.id }}
|
||||
permissions: {}
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-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@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -69,6 +76,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: staging
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -120,6 +128,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -139,7 +148,9 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
run: |
|
||||
yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
git add package.json
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
@@ -161,11 +172,6 @@ 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"
|
||||
|
||||
@@ -273,13 +279,18 @@ jobs:
|
||||
needs: release
|
||||
if: inputs.npm
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
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 +314,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
|
||||
|
||||
@@ -8,10 +8,14 @@ on:
|
||||
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:
|
||||
@@ -25,18 +29,19 @@ jobs:
|
||||
with:
|
||||
cache: "yarn"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c # v3.1.1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
tag: next
|
||||
ignore-scripts: false
|
||||
run: |
|
||||
npm publish --provenance --access public --tag next
|
||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||
echo "id=$release" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
|
||||
|
||||
@@ -21,20 +21,56 @@ on:
|
||||
type: boolean
|
||||
default: true
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
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:
|
||||
repo:
|
||||
- element-hq/element-web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
docs:
|
||||
name: Publish Documentation
|
||||
needs: release
|
||||
if: inputs.docs
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -43,6 +79,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -59,7 +96,7 @@ jobs:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: docs
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
|
||||
@@ -5,23 +5,29 @@ on:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
# No longer used
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
required: false
|
||||
inputs:
|
||||
sharded:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-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@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
@@ -40,7 +46,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: coverage
|
||||
path: coverage
|
||||
@@ -48,22 +54,28 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
merge-multiple: true
|
||||
- name: Check coverage artifact
|
||||
run: |
|
||||
if [ ! -d coverage ]; then
|
||||
echo "Coverage not found. Exiting with failure."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- id: extra_args
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
reports=$(find coverage -type f -name '*sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v3.2
|
||||
uses: matrix-org/sonarcloud-workflow-action@v4.0
|
||||
# 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:
|
||||
@@ -75,7 +87,7 @@ jobs:
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
- uses: guibranco/github-status-action-v2@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -7,10 +7,15 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
@@ -8,16 +8,18 @@ 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
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
@@ -25,25 +27,16 @@ jobs:
|
||||
- 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"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
@@ -51,8 +44,8 @@ jobs:
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
node_example_lint:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -60,6 +53,42 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build Types
|
||||
run: "yarn build:types"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
node-version-file: "examples/node/package.json"
|
||||
# cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install Example Deps
|
||||
run: "npm install"
|
||||
working-directory: "examples/node"
|
||||
|
||||
- name: Check Syntax
|
||||
run: "node --check app.js"
|
||||
working-directory: "examples/node"
|
||||
|
||||
- name: Typecheck
|
||||
run: "npx tsc"
|
||||
working-directory: "examples/node"
|
||||
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -69,19 +98,20 @@ jobs:
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors"
|
||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -93,16 +123,51 @@ jobs:
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
|
||||
element-web:
|
||||
name: Downstream tsc element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
# Hook for branch protection to skip downstream typechecking outside of merge queues
|
||||
downstream:
|
||||
name: Downstream Typescript Syntax Check
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- element-web
|
||||
steps:
|
||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -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@develop
|
||||
with:
|
||||
LABELS: |
|
||||
element-hq/element-meta
|
||||
.github/labels.yml
|
||||
DELETE: true
|
||||
WET: true
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
+26
-10
@@ -10,15 +10,16 @@ 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
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
specs: [integ, unit]
|
||||
node: [18, "lts/*", 21]
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -63,10 +64,22 @@ jobs:
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
# Dummy completion job to simplify branch protections
|
||||
jest-complete:
|
||||
name: Jest tests
|
||||
needs: jest
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
element-web:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop
|
||||
permissions:
|
||||
statuses: write
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
@@ -74,6 +87,7 @@ jobs:
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
if: github.event_name == 'merge_group'
|
||||
permissions: read-all
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
||||
with:
|
||||
use_js_sdk: "."
|
||||
@@ -81,7 +95,7 @@ jobs:
|
||||
# we need this so the job is reported properly when run in a merge queue
|
||||
downstream-complement-crypto:
|
||||
name: Downstream Complement Crypto tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- complement-crypto
|
||||
@@ -93,14 +107,16 @@ jobs:
|
||||
# and skip sonarcloud coverage within merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
- element-web
|
||||
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@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
@@ -109,5 +125,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,10 +3,10 @@ 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
|
||||
with:
|
||||
|
||||
@@ -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@develop
|
||||
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@v9
|
||||
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."
|
||||
+411
@@ -1,3 +1,414 @@
|
||||
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
|
||||
|
||||
+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 Jest.
|
||||
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.
|
||||
|
||||
@@ -38,8 +38,8 @@ client.publicRooms(function (err, data) {
|
||||
});
|
||||
```
|
||||
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
See [below](#end-to-end-encryption-support) for how to enable end-to-end-encryption, or check
|
||||
[the Node.js terminal app](https://github.com/matrix-org/matrix-js-sdk/tree/develop/examples/node) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
@@ -95,7 +95,7 @@ Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
|
||||
## Authenticated media
|
||||
|
||||
Servers supporting [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) will require clients, like
|
||||
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.
|
||||
@@ -126,26 +126,26 @@ const img = await fetch(downloadUrl, {
|
||||
This SDK provides a full object model around the Matrix Client-Server API and emits
|
||||
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
|
||||
|
||||
- Handles syncing (via `/sync`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
- Handles syncing (via `/sync`)
|
||||
- Handles the generation of "friendly" room and member names.
|
||||
- Handles historical `RoomMember` information (e.g. display names).
|
||||
- Manages room member state across multiple events (e.g. it handles typing, power
|
||||
levels and membership changes).
|
||||
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
|
||||
which can be listened to for things like name changes, new messages, membership
|
||||
changes, presence changes, and more.
|
||||
- Handle "local echo" of messages sent using the SDK. This means that messages
|
||||
that have just been sent will appear in the timeline as 'sending', until it
|
||||
completes. This is beneficial because it prevents there being a gap between
|
||||
hitting the send button and having the "remote echo" arrive.
|
||||
- Mark messages which failed to send as not sent.
|
||||
- Automatically retry requests to send messages due to network errors.
|
||||
- Automatically retry requests to send messages due to rate limiting errors.
|
||||
- Handle queueing of messages.
|
||||
- Handles pagination.
|
||||
- Handle assigning push actions for events.
|
||||
- Handles room initial sync on accepting invites.
|
||||
- Handles WebRTC calling.
|
||||
|
||||
# Usage
|
||||
|
||||
@@ -154,7 +154,7 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
|
||||
`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.
|
||||
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
|
||||
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officially supported.
|
||||
|
||||
## Emitted events
|
||||
|
||||
@@ -191,6 +191,7 @@ As well as the primary entry point (`matrix-js-sdk`), there are several other en
|
||||
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
|
||||
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
|
||||
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
|
||||
| `matrix-js-sdk/lib/utils/*.js` | A set of modules exporting standalone functions (and their types). |
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -302,44 +303,131 @@ Then visit `http://localhost:8005` to see the API docs.
|
||||
|
||||
# End-to-end encryption support
|
||||
|
||||
**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support
|
||||
from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).
|
||||
`matrix-js-sdk`'s end-to-end encryption support is based on the [WebAssembly bindings](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm) of the Rust [matrix-sdk-crypto](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto) library.
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the `Olm` global.
|
||||
## Initialization
|
||||
|
||||
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
|
||||
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
|
||||
initialise the crypto layer.
|
||||
To initialize the end-to-end encryption support in the matrix client:
|
||||
|
||||
If the `Olm` global is not available, the SDK will show a warning, as shown
|
||||
below; `initCrypto()` will also fail.
|
||||
```javascript
|
||||
// Create a new matrix client
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId,
|
||||
});
|
||||
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
// 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
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
// We want to output ES modules for the final build (mostly to ensure that
|
||||
// async imports work correctly). However, jest doesn't support ES modules very
|
||||
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
|
||||
// when testing.
|
||||
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/preset-typescript",
|
||||
{
|
||||
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
|
||||
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
|
||||
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
|
||||
// `.js` during transpilation, *except* when we are targetting Jest.
|
||||
rewriteImportExtensions: process.env.NODE_ENV !== "test",
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
["@babel/plugin-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",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
## 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: Optional<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 `Optional<X>` (`type Optional<T> = T | null | undefined`) 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: Optional<string>, // 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. Jest 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.
|
||||
+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"]
|
||||
}
|
||||
+1
-1
@@ -39,7 +39,7 @@ if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
reporters.push("<rootDir>/spec/slowReporter.js");
|
||||
reporters.push("<rootDir>/spec/slowReporter.cjs");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,21 @@ export default {
|
||||
"src/types.ts",
|
||||
"src/browser-index.ts",
|
||||
"src/indexeddb-worker.ts",
|
||||
"src/crypto-api/index.ts",
|
||||
"src/testing.ts",
|
||||
"src/matrix.ts",
|
||||
"scripts/**",
|
||||
"spec/**",
|
||||
"release.sh",
|
||||
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
|
||||
"src/**",
|
||||
// XXX: these look entirely unused
|
||||
"src/crypto/aes.ts",
|
||||
"src/crypto/crypto.ts",
|
||||
"src/crypto/recoverykey.ts",
|
||||
// XXX: these should be re-exported by one of the supported exports
|
||||
"src/matrixrtc/index.ts",
|
||||
"src/sliding-sync.ts",
|
||||
"src/webrtc/groupCall.ts",
|
||||
"src/webrtc/stats/media/mediaTrackStats.ts",
|
||||
"src/rendezvous/RendezvousChannel.ts",
|
||||
],
|
||||
project: ["**/*.{js,ts}"],
|
||||
ignore: ["examples/**"],
|
||||
@@ -33,4 +43,6 @@ export default {
|
||||
"dist",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
includeEntryExports: false,
|
||||
exclude: ["enumMembers"],
|
||||
} satisfies KnipConfig;
|
||||
|
||||
+34
-37
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "32.3.0",
|
||||
"version": "37.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "yarn build",
|
||||
@@ -26,18 +26,15 @@
|
||||
},
|
||||
"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": [
|
||||
@@ -53,19 +50,20 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^4.9.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^14.0.1",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.6.0",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "9"
|
||||
"uuid": "11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
@@ -74,41 +72,41 @@
|
||||
"@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",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@stylistic/eslint-plugin": "^4.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/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"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-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^48.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"eslint-plugin-jsdoc": "^50.0.0",
|
||||
"eslint-plugin-matrix-org": "^2.1.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock": "11.1.5",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
@@ -119,19 +117,18 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.2.5",
|
||||
"rimraf": "^5.0.0",
|
||||
"prettier": "3.5.1",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.25.10",
|
||||
"typedoc": "^0.27.0",
|
||||
"typedoc-plugin-coverage": "^3.0.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||
"typedoc-plugin-missing-exports": "^3.0.0",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,14 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split("/");
|
||||
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
|
||||
parseReleaseNotes(release.body, sections);
|
||||
for (const dependency of dependencies) {
|
||||
const releases = await getReleases(github, dependency);
|
||||
for (const release of releases) {
|
||||
@@ -120,12 +127,6 @@ const main = async ({ github, releaseId, dependencies }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
|
||||
|
||||
let output = "";
|
||||
@@ -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();
|
||||
+10
-15
@@ -26,14 +26,17 @@ 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
|
||||
@@ -55,10 +58,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 +65,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;
|
||||
|
||||
@@ -18,13 +18,13 @@ import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
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, 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 +37,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 +55,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} */
|
||||
@@ -107,7 +104,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
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,
|
||||
@@ -162,24 +159,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -263,7 +260,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
expect(calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
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
|
||||
@@ -272,7 +269,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
|
||||
// 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" };
|
||||
@@ -420,9 +417,8 @@ 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"),
|
||||
url: new RegExp("/_matrix/client/v3/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
(url, options) => {
|
||||
@@ -475,9 +471,6 @@ 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();
|
||||
});
|
||||
|
||||
+414
-1219
File diff suppressed because it is too large
Load Diff
@@ -17,11 +17,13 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
import { ClientEvent, createClient, 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 () => {
|
||||
@@ -40,6 +42,12 @@ describe("Device dehydration", () => {
|
||||
|
||||
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
||||
|
||||
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
|
||||
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
|
||||
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
|
||||
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
|
||||
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
|
||||
|
||||
// count the number of times the dehydration key gets set
|
||||
let setDehydrationCount = 0;
|
||||
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
|
||||
@@ -74,6 +82,8 @@ describe("Device dehydration", () => {
|
||||
await crypto.startDehydration();
|
||||
|
||||
expect(dehydrationCount).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
|
||||
// a week later, we should have created another dehydrated device
|
||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -81,7 +91,10 @@ describe("Device dehydration", () => {
|
||||
});
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await dehydrationPromise;
|
||||
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationCount).toEqual(2);
|
||||
expect(creationEventCounter.counter).toEqual(2);
|
||||
|
||||
// restart dehydration -- rehydrate the device that we created above,
|
||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||
@@ -113,6 +126,39 @@ describe("Device dehydration", () => {
|
||||
expect(setDehydrationCount).toEqual(2);
|
||||
expect(eventsResponse.mock.calls).toHaveLength(2);
|
||||
|
||||
expect(rehydrationStartedCounter.counter).toEqual(1);
|
||||
expect(rehydrationCompletedCounter.counter).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(3);
|
||||
expect(rehydrationProgressCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||
|
||||
// test that if we get an error when we try to rotate, it emits an event
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
});
|
||||
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await rotationErrorEventPromise;
|
||||
|
||||
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
});
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
return {};
|
||||
});
|
||||
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||
await crypto.startDehydration(true);
|
||||
await rehydrationErrorEventPromise;
|
||||
|
||||
matrixClient.stopClient();
|
||||
});
|
||||
});
|
||||
@@ -172,8 +218,8 @@ async function initializeSecretStorage(
|
||||
privateKey: new Uint8Array(32),
|
||||
};
|
||||
}
|
||||
await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await matrixClient.bootstrapSecretStorage({
|
||||
await matrixClient.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await matrixClient.getCrypto()!.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage: true,
|
||||
setupNewKeyBackup: false,
|
||||
|
||||
@@ -17,35 +17,28 @@ limitations under the License.
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { Mocked } from "jest-mock";
|
||||
import { type Mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
createClient,
|
||||
CryptoApi,
|
||||
CryptoEvent,
|
||||
ICreateClientOpts,
|
||||
IEvent,
|
||||
IMegolmSessionData,
|
||||
MatrixClient,
|
||||
encodeBase64,
|
||||
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 { DecryptionFailureCode } from "../../../src/crypto-api";
|
||||
import { defer, type IDeferred } from "../../../src/utils";
|
||||
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent, type CryptoApi } from "../../../src/crypto-api";
|
||||
import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||
|
||||
@@ -91,7 +84,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
@@ -114,12 +107,7 @@ function mockUploadEmitter(
|
||||
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;
|
||||
@@ -165,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;
|
||||
}
|
||||
@@ -246,11 +234,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
// On the first decryption attempt, decryption fails.
|
||||
await awaitDecryption(event);
|
||||
expect(event.decryptionFailureReason).toEqual(
|
||||
backend === "libolm"
|
||||
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
|
||||
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
|
||||
);
|
||||
expect(event.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
|
||||
|
||||
// Eventually, decryption succeeds.
|
||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
@@ -314,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()!;
|
||||
@@ -338,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);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -399,7 +367,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
it("Should import full backup in chunks", async function () {
|
||||
const importMockImpl = jest.fn();
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
|
||||
jest.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]);
|
||||
@@ -408,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 = jest.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);
|
||||
@@ -451,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 = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// Fail to import first chunk
|
||||
@@ -461,22 +427,21 @@ 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
|
||||
jest.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();
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
{
|
||||
progressCallback,
|
||||
},
|
||||
);
|
||||
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
// A chunk failed to import
|
||||
@@ -527,40 +492,22 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
|
||||
|
||||
const check = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
testData.BACKUP_DECRYPTION_KEY_BASE58,
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const result = await 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",
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
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
|
||||
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
|
||||
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!,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("Fails on bad recovery key", async function () {
|
||||
const fullBackup = {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
@@ -570,19 +517,18 @@ 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();
|
||||
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
|
||||
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);
|
||||
|
||||
await expect(
|
||||
aliceClient.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
|
||||
undefined,
|
||||
undefined,
|
||||
check!.backupInfo!,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
const result = await aliceCrypto.restoreKeyBackup();
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -796,7 +742,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
jest.runAllTimers();
|
||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||
await failurePromise;
|
||||
|
||||
// Fix the endpoint to do successful uploads
|
||||
@@ -829,7 +775,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
|
||||
});
|
||||
|
||||
// run the timers, which will make the backup loop redo the request
|
||||
await jest.runAllTimersAsync();
|
||||
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
|
||||
await successPromise;
|
||||
await allKeysUploadedPromise;
|
||||
});
|
||||
@@ -890,6 +836,40 @@ 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.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceClient.startClient();
|
||||
|
||||
// tell Alice to trust the dummy device that signed the backup
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
|
||||
// At this point there is no backup
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||
|
||||
// Return now the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// Delete the backup and we are expecting the key backup to be disabled
|
||||
const keyBackupStatus = defer<boolean>();
|
||||
aliceClient.once(CryptoEvent.KeyBackupStatus, (enabled) => keyBackupStatus.resolve(enabled));
|
||||
await aliceCrypto.deleteKeyBackupVersion(testData.SIGNED_BACKUP_DATA.version!);
|
||||
expect(await keyBackupStatus.promise).toBe(false);
|
||||
|
||||
// The backup info should not be available anymore
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
|
||||
});
|
||||
|
||||
describe("isKeyBackupTrusted", () => {
|
||||
it("does not trust a backup signed by an untrusted device", async () => {
|
||||
aliceClient = await initTestClient();
|
||||
@@ -965,7 +945,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()!;
|
||||
|
||||
|
||||
@@ -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, MsgType } 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: MsgType.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();
|
||||
});
|
||||
});
|
||||
@@ -17,11 +17,19 @@ limitations under the License.
|
||||
import Olm from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
|
||||
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";
|
||||
|
||||
/**
|
||||
* @module
|
||||
@@ -85,15 +93,15 @@ export function bootstrapCrossSigningTestOlmAccount(
|
||||
deviceId: string,
|
||||
keyBackupInfo: KeyBackupInfo[] = [],
|
||||
): Partial<IDownloadKeyResult> {
|
||||
const olmAliceMSK = new global.Olm.PkSigning();
|
||||
const olmAliceMSK = new globalThis.Olm.PkSigning();
|
||||
const masterPrivkey = olmAliceMSK.generate_seed();
|
||||
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
|
||||
|
||||
const olmAliceUSK = new global.Olm.PkSigning();
|
||||
const olmAliceUSK = new globalThis.Olm.PkSigning();
|
||||
const userPrivkey = olmAliceUSK.generate_seed();
|
||||
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
|
||||
|
||||
const olmAliceSSK = new global.Olm.PkSigning();
|
||||
const olmAliceSSK = new globalThis.Olm.PkSigning();
|
||||
const sskPrivkey = olmAliceSSK.generate_seed();
|
||||
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
|
||||
|
||||
@@ -181,7 +189,7 @@ export async function createOlmSession(
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
const session = new globalThis.Olm.Session();
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
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);
|
||||
|
||||
@@ -65,18 +67,36 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
|
||||
});
|
||||
|
||||
it("should create the meta db if given a pickleKey", async () => {
|
||||
it("should create the meta db if given a storageKey", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
pickleKey: "testKey",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
expect(databaseNames).toEqual(
|
||||
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should create the meta db if given a storagePassword", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
});
|
||||
|
||||
// No databases.
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
|
||||
await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" });
|
||||
|
||||
// should have two indexed dbs now
|
||||
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
|
||||
@@ -266,6 +286,38 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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 = jest.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
|
||||
// Then no error occurs, and no upgrade happens
|
||||
expect(progressListener.mock.calls.length).toBe(0);
|
||||
}, 60000);
|
||||
|
||||
describe("Legacy trust migration", () => {
|
||||
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
|
||||
const testStoreName = "test-store";
|
||||
@@ -399,10 +451,9 @@ describe("MatrixClient.clearStores", () => {
|
||||
baseUrl: "http://test.server",
|
||||
userId: "@alice:localhost",
|
||||
deviceId: "aliceDevice",
|
||||
pickleKey: "testKey",
|
||||
});
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
await matrixClient.initRustCrypto({ storagePassword: "testKey" });
|
||||
expect(await indexedDB.databases()).toHaveLength(2);
|
||||
await matrixClient.stopClient();
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { createClient, type MatrixClient } from "../../../src";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
/**
|
||||
* Integration tests for to-device messages functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe("to-device-messages", () => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: testData.TEST_USER_ID,
|
||||
accessToken: "akjgkrgjsalice",
|
||||
deviceId: testData.TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
const syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
// add bob as known user
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
|
||||
|
||||
// Silence warnings from the backup manager
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND" },
|
||||
});
|
||||
|
||||
fetchMock.get(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.get(new URL("/_matrix/client/versions/", homeserverUrl).toString(), {});
|
||||
fetchMock.post(
|
||||
new URL(
|
||||
`/_matrix/client/v3/user/${encodeURIComponent(testData.TEST_USER_ID)}/filter`,
|
||||
homeserverUrl,
|
||||
).toString(),
|
||||
{ filter_id: "fid" },
|
||||
);
|
||||
|
||||
await 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();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
it("returns empty batch for device that is not known", async () => {
|
||||
await aliceClient.startClient();
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch).toBeDefined();
|
||||
const { batch, eventType } = toDeviceBatch!;
|
||||
expect(eventType).toBe("m.room.encrypted");
|
||||
expect(batch.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns encrypted batch for known device", async () => {
|
||||
await aliceClient.startClient();
|
||||
e2eKeyResponder.addDeviceKeys(testData.BOB_SIGNED_TEST_DEVICE_DATA);
|
||||
fetchMock.post("express:/_matrix/client/v3/keys/claim", () => ({
|
||||
one_time_keys: testData.BOB_ONE_TIME_KEYS,
|
||||
}));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceBatch = await aliceClient
|
||||
.getCrypto()
|
||||
?.encryptToDeviceMessages(
|
||||
"m.test.event",
|
||||
[{ userId: testData.BOB_TEST_USER_ID, deviceId: testData.BOB_TEST_DEVICE_ID }],
|
||||
{
|
||||
some: "content",
|
||||
},
|
||||
);
|
||||
|
||||
expect(toDeviceBatch?.batch.length).toBe(1);
|
||||
expect(toDeviceBatch?.eventType).toBe("m.room.encrypted");
|
||||
const { deviceId, payload, userId } = toDeviceBatch!.batch[0];
|
||||
expect(deviceId).toBe(testData.BOB_TEST_DEVICE_ID);
|
||||
expect(userId).toBe(testData.BOB_TEST_USER_ID);
|
||||
expect(payload.algorithm).toBe("m.olm.v1.curve25519-aes-sha2");
|
||||
expect(payload.sender_key).toEqual(expect.any(String));
|
||||
expect(payload.ciphertext).toEqual(
|
||||
expect.objectContaining({
|
||||
[testData.BOB_SIGNED_TEST_DEVICE_DATA.keys[`curve25519:${testData.BOB_TEST_DEVICE_ID}`]]: {
|
||||
body: expect.any(String),
|
||||
type: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// for future: check that bob's device can decrypt the ciphertext?
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,42 +17,35 @@ 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 { IDBFactory } from "fake-indexeddb";
|
||||
import { createHash } from "crypto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
import {
|
||||
createClient,
|
||||
CryptoEvent,
|
||||
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 { awaitDecryption, emitPromise, getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
BACKUP_DECRYPTION_KEY_BASE64,
|
||||
@@ -78,9 +71,10 @@ 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
|
||||
@@ -90,11 +84,12 @@ jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
await global.Olm.init();
|
||||
await globalThis.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);
|
||||
@@ -116,11 +111,7 @@ const TEST_HOMESERVER_URL = "https://alice-server.com";
|
||||
* 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;
|
||||
|
||||
@@ -263,7 +254,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 globalThis.Olm.SAS();
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
@@ -357,7 +348,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 globalThis.Olm.SAS();
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||
|
||||
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||
@@ -430,9 +421,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 +462,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 +510,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
|
||||
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 +551,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);
|
||||
@@ -804,7 +790,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 +889,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);
|
||||
@@ -986,9 +971,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]);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1136,43 +1130,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
|
||||
jest.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", () => {
|
||||
@@ -1244,7 +1235,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
newBackendOnly("Should request cross signing keys after verification", async () => {
|
||||
it("Should request cross signing keys after verification", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
await doInteractiveVerification();
|
||||
@@ -1255,7 +1246,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 +1265,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 +1310,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 +1328,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,6 +1342,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
await sendBackupGossipAndExpectVersion(requestId!, "InvalidSecret", matchingBackupInfo);
|
||||
|
||||
// the backup secret should not be cached
|
||||
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..
|
||||
jest.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
@@ -1360,19 +1358,22 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
});
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// the backup secret should not be cached
|
||||
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
expect(cachedKey).toBeNull();
|
||||
});
|
||||
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);
|
||||
|
||||
@@ -1392,6 +1393,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
"express:/_matrix/client/v3/room_keys/version",
|
||||
(url, request) => {
|
||||
resolve(undefined);
|
||||
if (expectBackup instanceof MatrixError) {
|
||||
return {
|
||||
status: expectBackup.httpStatus,
|
||||
body: expectBackup.data,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectBackup instanceof Error) {
|
||||
return Promise.reject(expectBackup);
|
||||
}
|
||||
|
||||
return expectBackup;
|
||||
},
|
||||
{
|
||||
@@ -1466,7 +1478,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
deviceId: "device_under_test",
|
||||
...opts,
|
||||
});
|
||||
await initCrypto(client);
|
||||
await client.initRustCrypto();
|
||||
await client.startClient();
|
||||
return client;
|
||||
}
|
||||
@@ -1511,7 +1523,7 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
|
||||
return new Promise((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return {};
|
||||
},
|
||||
@@ -1535,7 +1547,7 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
@@ -1626,7 +1638,7 @@ function buildReadyMessage(
|
||||
}
|
||||
|
||||
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
|
||||
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
|
||||
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) {
|
||||
return {
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
@@ -1722,7 +1734,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 +1750,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";
|
||||
@@ -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
|
||||
|
||||
@@ -13,28 +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,
|
||||
JoinRule,
|
||||
KnockRoomOpts,
|
||||
type KnockRoomOpts,
|
||||
MemoryStore,
|
||||
Method,
|
||||
Room,
|
||||
RoomSummary,
|
||||
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";
|
||||
|
||||
@@ -168,14 +166,17 @@ describe("MatrixClient", function () {
|
||||
type: "test",
|
||||
content: {},
|
||||
});
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
room.addLiveEvents(
|
||||
[
|
||||
utils.mkMembership({
|
||||
user: userId,
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
event: true,
|
||||
}),
|
||||
],
|
||||
{ addToState: true },
|
||||
);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
store.storeRoom(room);
|
||||
|
||||
@@ -188,14 +189,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
|
||||
@@ -257,7 +261,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 });
|
||||
|
||||
@@ -275,14 +279,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);
|
||||
@@ -635,126 +642,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(() => {
|
||||
@@ -1293,18 +1180,109 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("getCapabilities", () => {
|
||||
it("should cache by default", async () => {
|
||||
it("should return cached capabilities if present", async () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: {
|
||||
"m.change_password": false,
|
||||
},
|
||||
capabilities: capsObject,
|
||||
});
|
||||
const prom = httpBackend.flushAllExpected();
|
||||
const capabilities1 = await client.getCapabilities();
|
||||
const capabilities2 = await client.getCapabilities();
|
||||
|
||||
client.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await client.getCapabilities()).toEqual(capsObject);
|
||||
});
|
||||
|
||||
it("should fetch capabilities if cache not present", async () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: capsObject,
|
||||
});
|
||||
|
||||
const capsPromise = client.getCapabilities();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(await capsPromise).toEqual(capsObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCachedCapabilities", () => {
|
||||
it("should return cached capabilities or undefined", async () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: capsObject,
|
||||
});
|
||||
|
||||
expect(client.getCachedCapabilities()).toBeUndefined();
|
||||
|
||||
client.startClient();
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(client.getCachedCapabilities()).toEqual(capsObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchCapabilities", () => {
|
||||
const capsObject = {
|
||||
"m.change_password": false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: capsObject,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should always fetch capabilities and then cache", async () => {
|
||||
const prom = client.fetchCapabilities();
|
||||
await httpBackend.flushAllExpected();
|
||||
const caps = await prom;
|
||||
|
||||
expect(caps).toEqual(capsObject);
|
||||
});
|
||||
|
||||
it("should write-through the cache", async () => {
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
client.startClient();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
expect(client.getCachedCapabilities()).toEqual(capsObject);
|
||||
|
||||
const newCapsObject = {
|
||||
"m.change_password": true,
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/capabilities").respond(200, {
|
||||
capabilities: newCapsObject,
|
||||
});
|
||||
|
||||
const prom = client.fetchCapabilities();
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
|
||||
expect(capabilities1).toStrictEqual(capabilities2);
|
||||
expect(client.getCachedCapabilities()).toEqual(newCapsObject);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1528,49 +1506,6 @@ 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();
|
||||
@@ -1815,6 +1750,28 @@ describe("MatrixClient", function () {
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDomain", () => {
|
||||
it("should return null if no userId is set", () => {
|
||||
const client = new MatrixClient({ baseUrl: "http://localhost" });
|
||||
expect(client.getDomain()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the domain of the userId", () => {
|
||||
expect(client.getDomain()).toBe("localhost");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserIdLocalpart", () => {
|
||||
it("should return null if no userId is set", () => {
|
||||
const client = new MatrixClient({ baseUrl: "http://localhost" });
|
||||
expect(client.getUserIdLocalpart()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the localpart of the userId", () => {
|
||||
expect(client.getUserIdLocalpart()).toBe("alice");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
@@ -1825,7 +1782,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",
|
||||
@@ -1846,12 +1802,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",
|
||||
@@ -1869,7 +1823,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) =>
|
||||
@@ -1909,7 +1862,6 @@ const buildEventRedaction = (event: MatrixEvent) =>
|
||||
|
||||
const buildEventPollStartThreadRoot = () =>
|
||||
new MatrixEvent({
|
||||
age: 80108647,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "ENCRYPTEDSTUFF",
|
||||
@@ -1923,12 +1875,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",
|
||||
@@ -1947,12 +1897,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",
|
||||
},
|
||||
@@ -1963,12 +1911,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",
|
||||
},
|
||||
@@ -1979,12 +1925,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",
|
||||
},
|
||||
@@ -1995,12 +1939,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",
|
||||
},
|
||||
@@ -2011,12 +1953,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,
|
||||
},
|
||||
@@ -2027,12 +1967,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: {
|
||||
@@ -2063,12 +2001,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",
|
||||
@@ -2081,12 +2017,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",
|
||||
},
|
||||
@@ -2097,13 +2031,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,
|
||||
|
||||
@@ -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, MatrixClient, MatrixScheduler, MsgType, RoomEvent } 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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
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;
|
||||
@@ -105,13 +105,13 @@ describe("MatrixClient syncing errors", () => {
|
||||
|
||||
await client!.startClient();
|
||||
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[1].promise).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
||||
});
|
||||
|
||||
@@ -119,6 +119,7 @@ describe("MatrixClient syncing errors", () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
fetchMock
|
||||
.get("end:capabilities", {})
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
|
||||
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
|
||||
|
||||
@@ -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,16 +24,15 @@ 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,
|
||||
@@ -47,9 +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 { IActionsObject } from "../../src/pushprocessor";
|
||||
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";
|
||||
@@ -112,7 +117,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
await client!.initRustCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -227,7 +232,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
|
||||
await client!.initCrypto();
|
||||
await client!.initRustCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -556,7 +561,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,
|
||||
@@ -577,7 +582,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -589,7 +594,7 @@ describe("MatrixClient syncing", () => {
|
||||
name: "The Ghost",
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -617,7 +622,7 @@ describe("MatrixClient syncing", () => {
|
||||
name: "The Ghost",
|
||||
}) as IMinimalEvent,
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -644,7 +649,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should no-op if resolveInvitesToProfiles is not set", () => {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
syncData.rooms.join[roomOne].state!.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Invite,
|
||||
@@ -1373,6 +1378,114 @@ describe("MatrixClient syncing", () => {
|
||||
expect(stateEventEmitCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("msc4222", () => {
|
||||
const roomOneSyncOne = {
|
||||
"timeline": {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: otherUserId,
|
||||
content: {
|
||||
name: "Initial room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Join,
|
||||
user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne,
|
||||
mship: KnownMembership.Join,
|
||||
user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: {},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
const roomOneSyncTwo = {
|
||||
"org.matrix.msc4222.state_after": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.topic",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { topic: "A new room topic" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
"state": {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name",
|
||||
room: roomOne,
|
||||
user: selfUserId,
|
||||
content: { name: "A new room name" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should ignore state events in timeline when state_after is present", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncOne },
|
||||
},
|
||||
});
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncTwo },
|
||||
},
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.name).toEqual("Initial room name");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect state events in state_after for left rooms", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
join: { [roomOne]: roomOneSyncOne },
|
||||
},
|
||||
});
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
rooms: {
|
||||
leave: { [roomOne]: roomOneSyncTwo },
|
||||
},
|
||||
});
|
||||
|
||||
client!.startClient();
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
|
||||
const room = client!.getRoom(roomOne)!;
|
||||
expect(room.name).toEqual("Initial room name");
|
||||
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
|
||||
"A new room topic",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline", () => {
|
||||
@@ -2274,6 +2387,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", () => {
|
||||
@@ -2281,67 +2445,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", () => {
|
||||
@@ -2402,16 +2569,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";
|
||||
|
||||
@@ -2483,7 +2649,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",
|
||||
}),
|
||||
});
|
||||
@@ -2555,7 +2721,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,
|
||||
@@ -128,7 +127,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
await room.addLiveEvents([thread.rootEvent]);
|
||||
await room.addLiveEvents([thread.rootEvent], { addToState: false });
|
||||
|
||||
// Initialize read receipt datastructure before testing the reaction
|
||||
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import {
|
||||
MSC4108FailureReason,
|
||||
MSC4108RendezvousSession,
|
||||
MSC4108SecureChannel,
|
||||
MSC4108SignInWithQR,
|
||||
PayloadType,
|
||||
RendezvousError,
|
||||
} from "../../../src/rendezvous";
|
||||
import { defer } from "../../../src/utils";
|
||||
import {
|
||||
ClientPrefix,
|
||||
DEVICE_CODE_SCOPE,
|
||||
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: jest.fn(),
|
||||
};
|
||||
const client = {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
return Promise.resolve(opts.msc4108Enabled && feature === "org.matrix.msc4108");
|
||||
},
|
||||
getUserId() {
|
||||
return opts.userId;
|
||||
},
|
||||
getDeviceId() {
|
||||
return opts.deviceId;
|
||||
},
|
||||
baseUrl,
|
||||
getDomain: () => "example.com",
|
||||
getDevice: jest.fn(),
|
||||
getCrypto: jest.fn(() => crypto),
|
||||
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||
} as unknown as MatrixClient;
|
||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||
baseUrl: client.baseUrl,
|
||||
prefix: ClientPrefix.Unstable,
|
||||
onlyData: true,
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("MSC4108SignInWithQR", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.get("https://issuer/jwks", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
const url = "https://fallbackserver/rz/123";
|
||||
const deviceId = "DEADB33F";
|
||||
const verificationUri = "https://example.com/verify";
|
||||
const verificationUriComplete = "https://example.com/verify/complete";
|
||||
|
||||
it("should generate qr code data as expected", async () => {
|
||||
const session = new MSC4108RendezvousSession({
|
||||
url,
|
||||
});
|
||||
const channel = new MSC4108SecureChannel(session);
|
||||
const login = new MSC4108SignInWithQR(channel, false);
|
||||
|
||||
await login.generateCode();
|
||||
const code = login.code;
|
||||
expect(code).toHaveLength(71);
|
||||
const text = new TextDecoder().decode(code);
|
||||
expect(text.startsWith("MATRIX")).toBeTruthy();
|
||||
expect(text.endsWith(url)).toBeTruthy();
|
||||
|
||||
// Assert that the code is stable
|
||||
await login.generateCode();
|
||||
expect(login.code).toEqual(code);
|
||||
});
|
||||
|
||||
describe("should be able to connect as a reciprocating device", () => {
|
||||
let client: MatrixClient;
|
||||
let ourLogin: MSC4108SignInWithQR;
|
||||
let opponentLogin: MSC4108SignInWithQR;
|
||||
|
||||
beforeEach(async () => {
|
||||
let ourData = defer<string>();
|
||||
let opponentData = defer<string>();
|
||||
|
||||
const ourMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
ourData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
const prom = opponentData.promise;
|
||||
prom.then(() => {
|
||||
opponentData = defer();
|
||||
});
|
||||
return prom;
|
||||
}),
|
||||
url,
|
||||
cancelled: false,
|
||||
cancel: () => {
|
||||
// @ts-ignore
|
||||
ourMockSession.cancelled = true;
|
||||
ourData.resolve("");
|
||||
},
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
const opponentMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
opponentData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
const prom = ourData.promise;
|
||||
prom.then(() => {
|
||||
ourData = defer();
|
||||
});
|
||||
return prom;
|
||||
}),
|
||||
url,
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
|
||||
client = makeMockClient({ userId: "@alice:example.com", deviceId: "alice", msc4108Enabled: true });
|
||||
|
||||
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
||||
const qrCodeData = QrCodeData.fromBytes(
|
||||
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
|
||||
);
|
||||
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
|
||||
|
||||
ourLogin = new MSC4108SignInWithQR(ourChannel, true, client);
|
||||
opponentLogin = new MSC4108SignInWithQR(opponentChannel, false);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share server name & check code", async () => {
|
||||
await Promise.all([
|
||||
expect(ourLogin.negotiateProtocols()).resolves.toEqual({}),
|
||||
expect(opponentLogin.negotiateProtocols()).resolves.toEqual({ serverName: client.getDomain() }),
|
||||
]);
|
||||
|
||||
expect(ourLogin.checkCode).toBe(opponentLogin.checkCode);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share verificationUri", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
|
||||
verificationUri: verificationUriComplete,
|
||||
}),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
verification_uri_complete: verificationUriComplete,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort if device already exists", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort on unsupported protocol", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow(
|
||||
"Received a request for an unsupported protocol",
|
||||
),
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
opponentLogin.send({
|
||||
type: PayloadType.Protocol,
|
||||
protocol: "device_authorization_grant_v2",
|
||||
device_authorization_grant: {
|
||||
verification_uri: verificationUri,
|
||||
},
|
||||
device_id: deviceId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should be able to connect with opponent and share secrets", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
|
||||
// Consume the ProtocolAccepted message which would normally be handled by step 4 which we do not have here
|
||||
// @ts-ignore
|
||||
await opponentLogin.receive();
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
|
||||
const payload = {
|
||||
secrets: expect.objectContaining(secrets),
|
||||
};
|
||||
await Promise.all([
|
||||
expect(ourProm).resolves.toEqual(payload),
|
||||
expect(opponentLogin.shareSecrets()).resolves.toEqual(payload),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
jest.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
// @ts-ignore
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
await expect(ourProm).rejects.toThrow("New device not found");
|
||||
});
|
||||
|
||||
it("should abort on unexpected errors", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
// We don't have the new device side of this flow implemented at this time so mock it
|
||||
// @ts-ignore
|
||||
ourLogin.expectingNewDeviceId = "DEADB33F";
|
||||
|
||||
// @ts-ignore
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
|
||||
);
|
||||
|
||||
await expect(ourLogin.shareSecrets()).rejects.toThrow("The message");
|
||||
});
|
||||
|
||||
it("should abort on declined login", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
await ourLogin.declineLoginOnExistingDevice();
|
||||
await expect(opponentLogin.shareSecrets()).rejects.toThrow(
|
||||
new RendezvousError("Failed", MSC4108FailureReason.UserCancelled),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
jest.spyOn(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 deferred = defer<IMyDevice>();
|
||||
mocked(client.getDevice).mockReturnValue(deferred.promise);
|
||||
|
||||
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
|
||||
deferred.resolve({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourProm).rejects.toThrow("User cancelled"),
|
||||
expect(opponentProm).rejects.toThrow("Unexpected message received"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,41 +15,56 @@ 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 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";
|
||||
|
||||
@@ -119,8 +134,9 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
await client!.initRustCrypto({ useIndexedDB: false });
|
||||
syncCryptoCallback = client!.getCrypto() as unknown as SyncCryptoCallbacks;
|
||||
syncOpts.cryptoCallbacks = syncCryptoCallback;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||
@@ -601,13 +617,13 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
initial: true,
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
|
||||
],
|
||||
timeline: [],
|
||||
});
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
await emitPromise(client!, RoomMemberEvent.Name);
|
||||
@@ -633,13 +649,6 @@ describe("SlidingSyncSdk", () => {
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
@@ -648,28 +657,28 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
client!.crypto!.processDeviceLists = jest.fn();
|
||||
syncCryptoCallback!.processDeviceLists = jest.fn();
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
},
|
||||
});
|
||||
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
|
||||
expect(syncCryptoCallback!.processDeviceLists).toHaveBeenCalledWith({
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
});
|
||||
});
|
||||
|
||||
it("can update OTK counts and unused fallback keys", () => {
|
||||
client!.crypto!.processKeyCounts = jest.fn();
|
||||
syncCryptoCallback!.processKeyCounts = jest.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
expect(syncCryptoCallback!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
"signed_curve25519",
|
||||
]);
|
||||
});
|
||||
@@ -921,13 +930,12 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with typing",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
required_state: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
|
||||
],
|
||||
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
|
||||
initial: true,
|
||||
});
|
||||
await emitPromise(client!, ClientEvent.Room);
|
||||
@@ -962,13 +970,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)!;
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import { logger } from "../src/logger";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
globalThis.Olm = require("@matrix-org/olm");
|
||||
logger.log("loaded libolm");
|
||||
} catch (e) {
|
||||
|
||||
@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import DOMException from "domexception";
|
||||
|
||||
global.DOMException = DOMException as typeof global.DOMException;
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||
|
||||
@@ -15,20 +15,26 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockOptionsMethodPut } from "fetch-mock";
|
||||
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* Intercept requests to set a particular type of account data.
|
||||
@@ -40,7 +46,10 @@ export class AccountDataAccumulator {
|
||||
* @param opts - options to pass to fetchMock
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public interceptSetAccountData(accountDataType: string, opts?: MockOptionsMethodPut): Promise<any> {
|
||||
public interceptSetAccountData(
|
||||
accountDataType: string,
|
||||
opts?: Parameters<(typeof fetchMock)["put"]>[2],
|
||||
): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
@@ -51,6 +60,9 @@ export class AccountDataAccumulator {
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
resolve(content);
|
||||
|
||||
// return a sync response
|
||||
this.sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
},
|
||||
opts,
|
||||
@@ -88,9 +100,9 @@ export class AccountDataAccumulator {
|
||||
/**
|
||||
* 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 +111,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.
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import { type Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
|
||||
@@ -17,9 +17,9 @@ limitations under the License.
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { IDownloadKeyResult } from "../../src";
|
||||
import { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
import { type IDownloadKeyResult } from "../../src";
|
||||
import { type IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
|
||||
/**
|
||||
* An object which intercepts `/keys/query` fetches via fetch-mock.
|
||||
|
||||
@@ -15,9 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import { type Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
@@ -80,7 +81,7 @@ export class SyncResponder implements ISyncResponder {
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<MockResponse> {
|
||||
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
|
||||
import { type MethodLikeKeys, mocked, type MockedObject } from "jest-mock";
|
||||
|
||||
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";
|
||||
|
||||
@@ -88,6 +88,6 @@ export const mockClientMethodsEvents = () => ({
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCapabilities: jest.fn().mockReturnValue({}),
|
||||
getCachedCapabilities: jest.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
@@ -16,14 +16,15 @@ limitations under the License.
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
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) {
|
||||
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
||||
{ versions: ["v1.1"] },
|
||||
@@ -35,7 +36,7 @@ export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
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" },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
@@ -88,7 +89,7 @@ export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
||||
});
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
|
||||
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
|
||||
+1
-36
@@ -14,39 +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",
|
||||
device_authorization_endpoint: issuer + "device",
|
||||
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",
|
||||
|
||||
@@ -208,7 +209,7 @@ def build_test_data(user_data, prefix = "") -> str:
|
||||
|
||||
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
|
||||
|
||||
return f"""\
|
||||
result = f"""\
|
||||
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
|
||||
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
|
||||
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
|
||||
@@ -239,7 +240,7 @@ export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
|
||||
json.dumps(build_cross_signing_keys_data(user_data, user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]), indent=4)
|
||||
};
|
||||
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
@@ -279,12 +280,20 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
|
||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
|
||||
"""
|
||||
|
||||
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
|
||||
if alt_master_key is not None:
|
||||
result += f"""
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const {prefix}ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(user_data, alt_master_key), indent=4)
|
||||
};
|
||||
"""
|
||||
|
||||
def build_cross_signing_keys_data(user_data) -> dict:
|
||||
return result
|
||||
|
||||
def build_cross_signing_keys_data(user_data, master_key_bytes) -> dict:
|
||||
"""Build the signed cross-signing-keys data for return from /keys/query"""
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
|
||||
)
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(master_key_bytes)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
@@ -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, type IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { type IDownloadKeyResult, type IEvent } from "../../../src";
|
||||
import { type KeyBackupSession, type KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -449,3 +449,50 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "eDZETBRUw9yW0WJnBZ7vxo12TW09Yb7/47qBPKZzPZzZEvs9M82dnAOtWUv00mcTdp2K9GpeFYDQJ6qLQgxaCA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@bob:xyz": {
|
||||
"keys": {
|
||||
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
|
||||
},
|
||||
"user_id": "@bob:xyz",
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:MCYxU7myKVkoQ55VYw/rXdg5cEupRfDdHmFPJUmR5+E": "Q1CbIXvp2BxBsu3F/eZ1ZpuR5rXIt0+FrrA/l6itskpW748xwMoIKxQRVQqs87kh7pCsWEoTy6FzIL8nV+P6BQ"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import EventEmitter from "events";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "../olm-loader";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type EventEmitter from "events";
|
||||
import { logger } from "../../src/logger";
|
||||
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import {
|
||||
type IContent,
|
||||
type IEvent,
|
||||
type IEventRelation,
|
||||
type IUnsigned,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../src/models/event";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventType,
|
||||
IJoinedRoom,
|
||||
IPusher,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
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";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -86,7 +92,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],
|
||||
@@ -127,7 +133,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
}
|
||||
} catch (ex) {
|
||||
} catch {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
// cause exceptions.
|
||||
// Overwriting will not work either in that case.
|
||||
@@ -173,8 +179,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,
|
||||
@@ -550,20 +558,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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## Dump of an empty libolm indexeddb cryptostore to test skipping migration
|
||||
|
||||
A dump of an account which is almost completely empty, and totally unsuitable
|
||||
for use as a real account.
|
||||
|
||||
This dump was manually created by copying and editing full_account.
|
||||
|
||||
Created to test
|
||||
["Unable to restore session" error due due to half-initialised legacy indexeddb crypto store #27447](https://github.com/element-hq/element-web/issues/27447).
|
||||
We should not launch the Rust migration code when we find a DB in this state.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"account": [],
|
||||
"device_data": [],
|
||||
"inbound_group_sessions": [],
|
||||
"inbound_group_sessions_withheld": [],
|
||||
"notified_error_devices": [],
|
||||
"outgoingRoomKeyRequests": [],
|
||||
"parked_shared_history": [],
|
||||
"rooms": [],
|
||||
"session_problems": [],
|
||||
"sessions": [],
|
||||
"sessions_needing_backup": [],
|
||||
"shared_history_inbound_group_sessions": []
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
* To be used during tests with fetchmock.
|
||||
*/
|
||||
const KEYS_QUERY_RESPONSE = { device_keys: { "@emptyuser:example.com": {} } };
|
||||
|
||||
/**
|
||||
* A dataset containing the information for the tested user.
|
||||
* To be used during tests.
|
||||
*/
|
||||
export const EMPTY_ACCOUNT_DATASET: DumpDataSetInfo = {
|
||||
userId: "@emptyuser:example.com",
|
||||
deviceId: "EMPTYDEVIC",
|
||||
pickleKey: "+/bcdefghijklmnopqrstu1/zyxvutsrqponmlkjih2",
|
||||
keyQueryResponse: KEYS_QUERY_RESPONSE,
|
||||
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/empty_account/dump.json",
|
||||
};
|
||||
@@ -1,4 +1,20 @@
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
/*
|
||||
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 { type DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
/*
|
||||
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 { type KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
|
||||
import { type DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
import { DumpDataSetInfo } from "../index";
|
||||
/*
|
||||
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 { type DumpDataSetInfo } from "../index";
|
||||
|
||||
/**
|
||||
* A key query response containing the current keys of the tested user.
|
||||
|
||||
@@ -15,10 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { RelationType } from "../../src/@types/event";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { type MatrixClient } from "../../src/client";
|
||||
import { type MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { type Room } from "../../src/models/room";
|
||||
import { type Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { mkMessage } from "./test-utils";
|
||||
|
||||
export const makeThreadEvent = ({
|
||||
@@ -178,6 +178,6 @@ export const populateThread = ({
|
||||
}: MakeThreadProps): MakeThreadResult => {
|
||||
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
|
||||
ret.thread.initialEventsFetched = true;
|
||||
room.addLiveEvents(ret.events);
|
||||
room.addLiveEvents(ret.events, { addToState: false });
|
||||
return ret;
|
||||
};
|
||||
|
||||
+35
-28
@@ -15,32 +15,33 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ClientEvent,
|
||||
ClientEventHandlerMap,
|
||||
type ClientEvent,
|
||||
type ClientEventHandlerMap,
|
||||
type EmptyObject,
|
||||
EventType,
|
||||
GroupCall,
|
||||
type GroupCall,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
IContent,
|
||||
ISendEventResponse,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
type IContent,
|
||||
type ISendEventResponse,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
type Room,
|
||||
RoomMember,
|
||||
RoomState,
|
||||
type RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
SendToDeviceContentMap,
|
||||
type RoomStateEventHandlerMap,
|
||||
type SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { CallEvent, CallEventHandlerMap, CallState, MatrixCall } from "../../src/webrtc/call";
|
||||
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
|
||||
import { CallFeed } from "../../src/webrtc/callFeed";
|
||||
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
|
||||
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
|
||||
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
|
||||
import { type CallEvent, type CallEventHandlerMap, CallState, type MatrixCall } from "../../src/webrtc/call";
|
||||
import { type CallEventHandlerEvent, type CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
|
||||
import { type CallFeed } from "../../src/webrtc/callFeed";
|
||||
import { type GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
|
||||
import { type GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
|
||||
import { type IScreensharingOpts, type MediaHandler } from "../../src/webrtc/mediaHandler";
|
||||
|
||||
export const DUMMY_SDP =
|
||||
"v=0\r\n" +
|
||||
@@ -147,10 +148,13 @@ export class MockRTCPeerConnection {
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
const localDescriptionJSON = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: "offer",
|
||||
toJSON: function () {},
|
||||
type: "offer" as RTCSdpType,
|
||||
};
|
||||
this.localDescription = {
|
||||
toJSON: () => localDescriptionJSON,
|
||||
...localDescriptionJSON,
|
||||
};
|
||||
|
||||
this.readyToNegotiate = new Promise<void>((resolve) => {
|
||||
@@ -265,7 +269,7 @@ export class MockRTCRtpTransceiver {
|
||||
this.peerConn.needsNegotiation = true;
|
||||
}
|
||||
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
@@ -463,7 +467,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
[roomId: string, eventType: EventType, content: any, statekey: string]
|
||||
>();
|
||||
public sendToDevice = jest.fn<
|
||||
Promise<{}>,
|
||||
Promise<EmptyObject>,
|
||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
||||
>();
|
||||
|
||||
@@ -478,6 +482,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
public getSafeUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public getDeviceId(): string {
|
||||
return this.deviceId;
|
||||
@@ -579,11 +586,11 @@ export class MockCallFeed {
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
global.navigator = {
|
||||
globalThis.navigator = {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
global.window = {
|
||||
globalThis.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
@@ -593,13 +600,13 @@ export function installWebRTCMocks() {
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
globalThis.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
globalThis.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpReceiver = {
|
||||
globalThis.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
@@ -607,7 +614,7 @@ export function installWebRTCMocks() {
|
||||
};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpSender = {
|
||||
globalThis.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ConnectionError } from "../../src/http-api/errors";
|
||||
import { ClientEvent, MatrixClient, Store } from "../../src/client";
|
||||
import { ClientEvent, type MatrixClient, type Store } from "../../src/client";
|
||||
import { ToDeviceMessageQueue } from "../../src/ToDeviceMessageQueue";
|
||||
import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { type IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
let shouldFailSendToDevice: Boolean;
|
||||
let shouldFailSendToDevice: boolean;
|
||||
let onSendToDeviceFailure: () => void;
|
||||
let onSendToDeviceSuccess: () => void;
|
||||
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
|
||||
|
||||
@@ -22,12 +22,12 @@ import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
// keep to reset the fetch function after using MockHttpBackend
|
||||
// @ts-ignore private property
|
||||
const realAutoDiscoveryFetch: typeof global.fetch = AutoDiscovery.fetchFn;
|
||||
const realAutoDiscoveryFetch: typeof globalThis.fetch = AutoDiscovery.fetchFn;
|
||||
|
||||
describe("AutoDiscovery", function () {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
const httpBackend = new MockHttpBackend();
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof globalThis.fetch);
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
@@ -857,7 +857,7 @@ describe("AutoDiscovery", function () {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
|
||||
error: AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
|
||||
@@ -15,34 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { TextEncoder, TextDecoder } from "util";
|
||||
import NodeBuffer from "node:buffer";
|
||||
|
||||
import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64";
|
||||
|
||||
describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
|
||||
let origBuffer = Buffer;
|
||||
|
||||
beforeAll(() => {
|
||||
if (env === "browser") {
|
||||
origBuffer = Buffer;
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = undefined;
|
||||
|
||||
global.atob = NodeBuffer.atob;
|
||||
global.btoa = NodeBuffer.btoa;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = origBuffer;
|
||||
// @ts-ignore
|
||||
global.atob = undefined;
|
||||
// @ts-ignore
|
||||
global.btoa = undefined;
|
||||
});
|
||||
|
||||
describe("Base64 encoding", () => {
|
||||
it("Should decode properly encoded data", () => {
|
||||
const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ="));
|
||||
|
||||
@@ -50,17 +26,18 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
|
||||
});
|
||||
|
||||
it("Should encode unpadded URL-safe base64", () => {
|
||||
const toEncode = "?????";
|
||||
// Chosen to have padding and multiple instances of / and + in the base64
|
||||
const toEncode = "???????⊕⊗⊗";
|
||||
const data = new TextEncoder().encode(toEncode);
|
||||
|
||||
const encoded = encodeUnpaddedBase64Url(data);
|
||||
expect(encoded).toEqual("Pz8_Pz8");
|
||||
expect(encoded).toEqual("Pz8_Pz8_P-KKleKKl-KKlw");
|
||||
});
|
||||
|
||||
it("Should decode URL-safe base64", () => {
|
||||
const decoded = new TextDecoder().decode(decodeBase64("Pz8_Pz8="));
|
||||
const decoded = new TextDecoder().decode(decodeBase64("Pz8_Pz8_P-KKleKKl-KKlw=="));
|
||||
|
||||
expect(decoded).toStrictEqual("?????");
|
||||
expect(decoded).toStrictEqual("???????⊕⊗⊗");
|
||||
});
|
||||
|
||||
it("Encode unpadded should not have padding", () => {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { keyFromAuthData } from "../../../src/common-crypto/key-passphrase.ts";
|
||||
|
||||
describe("key-passphrase", () => {
|
||||
describe("keyFromAuthData", () => {
|
||||
it("should throw an error if salt or iterations are missing", async () => {
|
||||
// missing salt
|
||||
expect(() => keyFromAuthData({ private_key_iterations: 5 }, "passphrase")).toThrow(
|
||||
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
|
||||
);
|
||||
|
||||
// missing iterations
|
||||
expect(() => keyFromAuthData({ private_key_salt: "salt" }, "passphrase")).toThrow(
|
||||
"Salt and/or iterations not found: this backup cannot be restored with a passphrase",
|
||||
);
|
||||
});
|
||||
|
||||
it("should derive key from auth data", async () => {
|
||||
const key = await keyFromAuthData({ private_key_salt: "salt", private_key_iterations: 5 }, "passphrase");
|
||||
expect(key).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
it("create fully defined event content", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
||||
@@ -207,6 +207,17 @@ describe("Topic content helpers", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates an empty event when the topic is falsey", () => {
|
||||
expect(makeTopicContent(undefined)).toEqual({
|
||||
topic: undefined,
|
||||
[M_TOPIC.name]: [],
|
||||
});
|
||||
expect(makeTopicContent(null)).toEqual({
|
||||
topic: null,
|
||||
[M_TOPIC.name]: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTopicContent()", () => {
|
||||
@@ -257,5 +268,26 @@ describe("Topic content helpers", () => {
|
||||
html: "<b>pizza</b>",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses legacy event content", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses legacy event content when new topic key is invalid", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
"topic": "pizza",
|
||||
"m.topic": {} as any,
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,42 +63,51 @@ describe("ContentRepo", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return an authenticated URL when requested", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
|
||||
baseUrl + "/_matrix/client/v1/media/download/server.name/resourceid?allow_redirect=true",
|
||||
);
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, true, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
|
||||
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should force-enable allow_redirects when useAuthentication is set true", function () {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, false, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
|
||||
baseUrl + "/_matrix/client/v1/media/download/server.name/resourceid?allow_redirect=true",
|
||||
);
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, false, true)).toEqual(
|
||||
baseUrl +
|
||||
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
|
||||
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should drop mxc urls with invalid server_name", () => {
|
||||
const mxcUri = "mxc://server.name:test/foobar";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls with invalid media_id", () => {
|
||||
const mxcUri = "mxc://server.name/foobar:test";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls attempting a path traversal attack", () => {
|
||||
const mxcUri = "mxc://../../../../foo";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls attempting to pass query parameters", () => {
|
||||
const mxcUri = "mxc://server.name/foobar?bar=baz";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
|
||||
it("should drop mxc urls with too many parts", () => {
|
||||
const mxcUri = "mxc://server.name/foo//bar";
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { decodeRecoveryKey, encodeRecoveryKey } from "../../../src/crypto-api";
|
||||
|
||||
describe("recovery key", () => {
|
||||
describe("decodeRecoveryKey", () => {
|
||||
it("should thrown an incorrect length error", () => {
|
||||
const key = [0, 1];
|
||||
const encodedKey = encodeRecoveryKey(key)!;
|
||||
|
||||
expect(() => decodeRecoveryKey(encodedKey)).toThrow("Incorrect length");
|
||||
});
|
||||
|
||||
it("should thrown an incorrect parity", () => {
|
||||
const key = Array.from({ length: 32 }, (_, i) => i);
|
||||
let encodedKey = encodeRecoveryKey(key)!;
|
||||
// Mutate the encoded key to have incorrect parity
|
||||
encodedKey = encodedKey.replace("EsSz", "EsSZ");
|
||||
|
||||
expect(() => decodeRecoveryKey(encodedKey!)).toThrow("Incorrect parity");
|
||||
});
|
||||
|
||||
it("should decode a valid encoded key", () => {
|
||||
const key = Array.from({ length: 32 }, (_, i) => i);
|
||||
const encodedKey = encodeRecoveryKey(key)!;
|
||||
|
||||
expect(decodeRecoveryKey(encodedKey)).toEqual(new Uint8Array(key));
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user