Commit 96fdf90aacd6
tarball32 files changed+17892-0
@@ -0,0 +1,2 @@
1
+/target2
+@@ -0,0 +1,4728 @@
1
+# This file is automatically @generated by Cargo.2
+# It is not intended for manual editing.3
+version = 44
+5
+[[package]]6
+name = "accessory"7
+version = "2.1.1"8
+source = "registry+https://github.com/rust-lang/crates.io-index"9
+checksum = "cc86a613208ca7d144ca24f6ec49c5cdfa5c05a46f2f7ab5880ff99371f165de"10
+dependencies = [11
+ "macroific 3.0.1",12
+ "proc-macro2",13
+ "quote",14
+ "syn 3.0.5",15
+]16
+17
+[[package]]18
+name = "adler2"19
+version = "2.0.1"20
+source = "registry+https://github.com/rust-lang/crates.io-index"21
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"22
+23
+[[package]]24
+name = "aead"25
+version = "0.5.2"26
+source = "registry+https://github.com/rust-lang/crates.io-index"27
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"28
+dependencies = [29
+ "crypto-common",30
+ "generic-array",31
+]32
+33
+[[package]]34
+name = "aes"35
+version = "0.8.4"36
+source = "registry+https://github.com/rust-lang/crates.io-index"37
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"38
+dependencies = [39
+ "cfg-if",40
+ "cipher",41
+ "cpufeatures 0.2.17",42
+]43
+44
+[[package]]45
+name = "aho-corasick"46
+version = "1.1.5"47
+source = "registry+https://github.com/rust-lang/crates.io-index"48
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"49
+dependencies = [50
+ "memchr",51
+]52
+53
+[[package]]54
+name = "android_system_properties"55
+version = "0.1.6"56
+source = "registry+https://github.com/rust-lang/crates.io-index"57
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"58
+dependencies = [59
+ "libc",60
+]61
+62
+[[package]]63
+name = "anyhow"64
+version = "1.0.104"65
+source = "registry+https://github.com/rust-lang/crates.io-index"66
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"67
+68
+[[package]]69
+name = "anymap2"70
+version = "0.13.0"71
+source = "registry+https://github.com/rust-lang/crates.io-index"72
+checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"73
+74
+[[package]]75
+name = "aquamarine"76
+version = "0.6.0"77
+source = "registry+https://github.com/rust-lang/crates.io-index"78
+checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2"79
+dependencies = [80
+ "include_dir",81
+ "itertools 0.10.5",82
+ "proc-macro-error2",83
+ "proc-macro2",84
+ "quote",85
+ "syn 2.0.119",86
+]87
+88
+[[package]]89
+name = "archery"90
+version = "1.2.3"91
+source = "registry+https://github.com/rust-lang/crates.io-index"92
+checksum = "33ca55ee147b1926dbea904f50fe4902494e97bc742205abbbf10c709e43815f"93
+94
+[[package]]95
+name = "arrayvec"96
+version = "0.7.8"97
+source = "registry+https://github.com/rust-lang/crates.io-index"98
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"99
+dependencies = [100
+ "serde",101
+]102
+103
+[[package]]104
+name = "as_variant"105
+version = "1.3.0"106
+source = "registry+https://github.com/rust-lang/crates.io-index"107
+checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a"108
+109
+[[package]]110
+name = "assert-json-diff"111
+version = "2.0.2"112
+source = "registry+https://github.com/rust-lang/crates.io-index"113
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"114
+dependencies = [115
+ "serde",116
+ "serde_json",117
+]118
+119
+[[package]]120
+name = "assert_matches"121
+version = "1.5.0"122
+source = "registry+https://github.com/rust-lang/crates.io-index"123
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"124
+125
+[[package]]126
+name = "assert_matches2"127
+version = "0.1.2"128
+source = "registry+https://github.com/rust-lang/crates.io-index"129
+checksum = "15832d94c458da98cac0ffa6eca52cc19c2a3c6c951058500a5ae8f01f0fdf56"130
+131
+[[package]]132
+name = "assign"133
+version = "1.1.1"134
+source = "registry+https://github.com/rust-lang/crates.io-index"135
+checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002"136
+137
+[[package]]138
+name = "async-channel"139
+version = "2.5.0"140
+source = "registry+https://github.com/rust-lang/crates.io-index"141
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"142
+dependencies = [143
+ "concurrent-queue",144
+ "event-listener-strategy",145
+ "futures-core",146
+ "pin-project-lite",147
+]148
+149
+[[package]]150
+name = "async-compression"151
+version = "0.4.45"152
+source = "registry+https://github.com/rust-lang/crates.io-index"153
+checksum = "24a8ec73eb862508b7041723c89386365894b4e7d9f6998bf1b8529e5b0ee254"154
+dependencies = [155
+ "compression-codecs",156
+ "compression-core",157
+ "pin-project-lite",158
+ "tokio",159
+]160
+161
+[[package]]162
+name = "async-once-cell"163
+version = "0.5.4"164
+source = "registry+https://github.com/rust-lang/crates.io-index"165
+checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"166
+167
+[[package]]168
+name = "async-rx"169
+version = "0.2.1"170
+source = "registry+https://github.com/rust-lang/crates.io-index"171
+checksum = "a82c1a761cd7b772912fd2500f2c0a850c0c72f5f66543038393ac6cfaa3659a"172
+dependencies = [173
+ "futures-core",174
+ "pin-project-lite",175
+]176
+177
+[[package]]178
+name = "async-stream"179
+version = "0.3.6"180
+source = "registry+https://github.com/rust-lang/crates.io-index"181
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"182
+dependencies = [183
+ "async-stream-impl",184
+ "futures-core",185
+ "pin-project-lite",186
+]187
+188
+[[package]]189
+name = "async-stream-impl"190
+version = "0.3.6"191
+source = "registry+https://github.com/rust-lang/crates.io-index"192
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"193
+dependencies = [194
+ "proc-macro2",195
+ "quote",196
+ "syn 2.0.119",197
+]198
+199
+[[package]]200
+name = "async-trait"201
+version = "0.1.92"202
+source = "registry+https://github.com/rust-lang/crates.io-index"203
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"204
+dependencies = [205
+ "proc-macro2",206
+ "quote",207
+ "syn 3.0.5",208
+]209
+210
+[[package]]211
+name = "async_cell"212
+version = "0.2.3"213
+source = "registry+https://github.com/rust-lang/crates.io-index"214
+checksum = "447ab28afbb345f5408b120702a44e5529ebf90b1796ec76e9528df8e288e6c2"215
+dependencies = [216
+ "loom",217
+]218
+219
+[[package]]220
+name = "atomic-waker"221
+version = "1.1.2"222
+source = "registry+https://github.com/rust-lang/crates.io-index"223
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"224
+225
+[[package]]226
+name = "autocfg"227
+version = "1.5.1"228
+source = "registry+https://github.com/rust-lang/crates.io-index"229
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"230
+231
+[[package]]232
+name = "aws-lc-rs"233
+version = "1.18.1"234
+source = "registry+https://github.com/rust-lang/crates.io-index"235
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"236
+dependencies = [237
+ "aws-lc-sys",238
+ "zeroize",239
+]240
+241
+[[package]]242
+name = "aws-lc-sys"243
+version = "0.45.0"244
+source = "registry+https://github.com/rust-lang/crates.io-index"245
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"246
+dependencies = [247
+ "cc",248
+ "cmake",249
+ "dunce",250
+ "fs_extra",251
+ "pkg-config",252
+]253
+254
+[[package]]255
+name = "backon"256
+version = "1.6.0"257
+source = "registry+https://github.com/rust-lang/crates.io-index"258
+checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"259
+dependencies = [260
+ "fastrand",261
+ "gloo-timers",262
+ "tokio",263
+]264
+265
+[[package]]266
+name = "base64"267
+version = "0.22.1"268
+source = "registry+https://github.com/rust-lang/crates.io-index"269
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"270
+271
+[[package]]272
+name = "base64ct"273
+version = "1.8.3"274
+source = "registry+https://github.com/rust-lang/crates.io-index"275
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"276
+277
+[[package]]278
+name = "bitflags"279
+version = "2.13.1"280
+source = "registry+https://github.com/rust-lang/crates.io-index"281
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"282
+dependencies = [283
+ "serde_core",284
+]285
+286
+[[package]]287
+name = "bitmaps"288
+version = "3.2.1"289
+source = "registry+https://github.com/rust-lang/crates.io-index"290
+checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"291
+292
+[[package]]293
+name = "blake3"294
+version = "1.8.7"295
+source = "registry+https://github.com/rust-lang/crates.io-index"296
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"297
+dependencies = [298
+ "arrayvec",299
+ "cc",300
+ "cfg-if",301
+ "constant_time_eq",302
+ "cpufeatures 0.3.1",303
+]304
+305
+[[package]]306
+name = "block-buffer"307
+version = "0.10.4"308
+source = "registry+https://github.com/rust-lang/crates.io-index"309
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"310
+dependencies = [311
+ "generic-array",312
+]313
+314
+[[package]]315
+name = "block-padding"316
+version = "0.3.3"317
+source = "registry+https://github.com/rust-lang/crates.io-index"318
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"319
+dependencies = [320
+ "generic-array",321
+]322
+323
+[[package]]324
+name = "bs58"325
+version = "0.5.1"326
+source = "registry+https://github.com/rust-lang/crates.io-index"327
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"328
+dependencies = [329
+ "tinyvec",330
+]331
+332
+[[package]]333
+name = "bumpalo"334
+version = "3.20.3"335
+source = "registry+https://github.com/rust-lang/crates.io-index"336
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"337
+338
+[[package]]339
+name = "byteorder"340
+version = "1.5.0"341
+source = "registry+https://github.com/rust-lang/crates.io-index"342
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"343
+344
+[[package]]345
+name = "bytes"346
+version = "1.12.1"347
+source = "registry+https://github.com/rust-lang/crates.io-index"348
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"349
+350
+[[package]]351
+name = "bytesize"352
+version = "2.7.0"353
+source = "registry+https://github.com/rust-lang/crates.io-index"354
+checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b"355
+356
+[[package]]357
+name = "cast"358
+version = "0.3.0"359
+source = "registry+https://github.com/rust-lang/crates.io-index"360
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"361
+362
+[[package]]363
+name = "cbc"364
+version = "0.1.2"365
+source = "registry+https://github.com/rust-lang/crates.io-index"366
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"367
+dependencies = [368
+ "cipher",369
+]370
+371
+[[package]]372
+name = "cc"373
+version = "1.4.5"374
+source = "registry+https://github.com/rust-lang/crates.io-index"375
+checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"376
+dependencies = [377
+ "find-msvc-tools",378
+ "jobserver",379
+ "libc",380
+ "shlex",381
+]382
+383
+[[package]]384
+name = "cfg-if"385
+version = "1.0.4"386
+source = "registry+https://github.com/rust-lang/crates.io-index"387
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"388
+389
+[[package]]390
+name = "cfg_aliases"391
+version = "0.2.2"392
+source = "registry+https://github.com/rust-lang/crates.io-index"393
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"394
+395
+[[package]]396
+name = "chacha20"397
+version = "0.9.1"398
+source = "registry+https://github.com/rust-lang/crates.io-index"399
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"400
+dependencies = [401
+ "cfg-if",402
+ "cipher",403
+ "cpufeatures 0.2.17",404
+]405
+406
+[[package]]407
+name = "chacha20"408
+version = "0.10.2"409
+source = "registry+https://github.com/rust-lang/crates.io-index"410
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"411
+dependencies = [412
+ "cfg-if",413
+ "cpufeatures 0.3.1",414
+ "rand_core 0.10.1",415
+]416
+417
+[[package]]418
+name = "chacha20poly1305"419
+version = "0.10.1"420
+source = "registry+https://github.com/rust-lang/crates.io-index"421
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"422
+dependencies = [423
+ "aead",424
+ "chacha20 0.9.1",425
+ "cipher",426
+ "poly1305",427
+ "zeroize",428
+]429
+430
+[[package]]431
+name = "chrono"432
+version = "0.4.45"433
+source = "registry+https://github.com/rust-lang/crates.io-index"434
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"435
+dependencies = [436
+ "iana-time-zone",437
+ "js-sys",438
+ "num-traits",439
+ "serde",440
+ "wasm-bindgen",441
+ "windows-link",442
+]443
+444
+[[package]]445
+name = "cipher"446
+version = "0.4.4"447
+source = "registry+https://github.com/rust-lang/crates.io-index"448
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"449
+dependencies = [450
+ "crypto-common",451
+ "inout",452
+ "zeroize",453
+]454
+455
+[[package]]456
+name = "cmake"457
+version = "0.1.58"458
+source = "registry+https://github.com/rust-lang/crates.io-index"459
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"460
+dependencies = [461
+ "cc",462
+]463
+464
+[[package]]465
+name = "combine"466
+version = "4.6.8"467
+source = "registry+https://github.com/rust-lang/crates.io-index"468
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"469
+dependencies = [470
+ "bytes",471
+ "memchr",472
+]473
+474
+[[package]]475
+name = "compression-codecs"476
+version = "0.4.40"477
+source = "registry+https://github.com/rust-lang/crates.io-index"478
+checksum = "100590da849306918656ffbb22576bbdfc1382b50ad10d1d50ec177d3b205fb2"479
+dependencies = [480
+ "compression-core",481
+ "flate2",482
+ "memchr",483
+]484
+485
+[[package]]486
+name = "compression-core"487
+version = "0.4.33"488
+source = "registry+https://github.com/rust-lang/crates.io-index"489
+checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414"490
+491
+[[package]]492
+name = "concurrent-queue"493
+version = "2.5.0"494
+source = "registry+https://github.com/rust-lang/crates.io-index"495
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"496
+dependencies = [497
+ "crossbeam-utils",498
+]499
+500
+[[package]]501
+name = "console"502
+version = "0.16.4"503
+source = "registry+https://github.com/rust-lang/crates.io-index"504
+checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"505
+dependencies = [506
+ "encode_unicode",507
+ "libc",508
+ "windows-sys 0.61.2",509
+]510
+511
+[[package]]512
+name = "const-oid"513
+version = "0.9.6"514
+source = "registry+https://github.com/rust-lang/crates.io-index"515
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"516
+517
+[[package]]518
+name = "const_panic"519
+version = "0.2.17"520
+source = "registry+https://github.com/rust-lang/crates.io-index"521
+checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3"522
+dependencies = [523
+ "typewit",524
+]525
+526
+[[package]]527
+name = "constant_time_eq"528
+version = "0.4.2"529
+source = "registry+https://github.com/rust-lang/crates.io-index"530
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"531
+532
+[[package]]533
+name = "core-foundation"534
+version = "0.10.1"535
+source = "registry+https://github.com/rust-lang/crates.io-index"536
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"537
+dependencies = [538
+ "core-foundation-sys",539
+ "libc",540
+]541
+542
+[[package]]543
+name = "core-foundation-sys"544
+version = "0.8.7"545
+source = "registry+https://github.com/rust-lang/crates.io-index"546
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"547
+548
+[[package]]549
+name = "cpufeatures"550
+version = "0.2.17"551
+source = "registry+https://github.com/rust-lang/crates.io-index"552
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"553
+dependencies = [554
+ "libc",555
+]556
+557
+[[package]]558
+name = "cpufeatures"559
+version = "0.3.1"560
+source = "registry+https://github.com/rust-lang/crates.io-index"561
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"562
+dependencies = [563
+ "libc",564
+]565
+566
+[[package]]567
+name = "crc32fast"568
+version = "1.5.1"569
+source = "registry+https://github.com/rust-lang/crates.io-index"570
+checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"571
+dependencies = [572
+ "cfg-if",573
+]574
+575
+[[package]]576
+name = "crossbeam-utils"577
+version = "0.8.23"578
+source = "registry+https://github.com/rust-lang/crates.io-index"579
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"580
+581
+[[package]]582
+name = "crypto-common"583
+version = "0.1.7"584
+source = "registry+https://github.com/rust-lang/crates.io-index"585
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"586
+dependencies = [587
+ "generic-array",588
+ "rand_core 0.6.4",589
+ "typenum",590
+]591
+592
+[[package]]593
+name = "ctor"594
+version = "0.2.9"595
+source = "registry+https://github.com/rust-lang/crates.io-index"596
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"597
+dependencies = [598
+ "quote",599
+ "syn 2.0.119",600
+]601
+602
+[[package]]603
+name = "ctr"604
+version = "0.9.2"605
+source = "registry+https://github.com/rust-lang/crates.io-index"606
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"607
+dependencies = [608
+ "cipher",609
+]610
+611
+[[package]]612
+name = "curve25519-dalek"613
+version = "4.1.3"614
+source = "registry+https://github.com/rust-lang/crates.io-index"615
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"616
+dependencies = [617
+ "cfg-if",618
+ "cpufeatures 0.2.17",619
+ "curve25519-dalek-derive",620
+ "digest",621
+ "fiat-crypto",622
+ "rustc_version",623
+ "serde",624
+ "subtle",625
+ "zeroize",626
+]627
+628
+[[package]]629
+name = "curve25519-dalek-derive"630
+version = "0.1.1"631
+source = "registry+https://github.com/rust-lang/crates.io-index"632
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"633
+dependencies = [634
+ "proc-macro2",635
+ "quote",636
+ "syn 2.0.119",637
+]638
+639
+[[package]]640
+name = "date_header"641
+version = "1.0.5"642
+source = "registry+https://github.com/rust-lang/crates.io-index"643
+checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d"644
+645
+[[package]]646
+name = "deadpool"647
+version = "0.12.3"648
+source = "registry+https://github.com/rust-lang/crates.io-index"649
+checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"650
+dependencies = [651
+ "deadpool-runtime 0.1.4",652
+ "lazy_static",653
+ "num_cpus",654
+ "tokio",655
+]656
+657
+[[package]]658
+name = "deadpool"659
+version = "0.13.1"660
+source = "registry+https://github.com/rust-lang/crates.io-index"661
+checksum = "3e98a7e119cd347f4201e1159b19831029e203e2d8b790547708e8157b4acf1e"662
+dependencies = [663
+ "deadpool-runtime 0.3.1",664
+ "tokio",665
+]666
+667
+[[package]]668
+name = "deadpool-runtime"669
+version = "0.1.4"670
+source = "registry+https://github.com/rust-lang/crates.io-index"671
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"672
+673
+[[package]]674
+name = "deadpool-runtime"675
+version = "0.3.1"676
+source = "registry+https://github.com/rust-lang/crates.io-index"677
+checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae"678
+dependencies = [679
+ "tokio",680
+]681
+682
+[[package]]683
+name = "deadpool-sync"684
+version = "0.2.0"685
+source = "registry+https://github.com/rust-lang/crates.io-index"686
+checksum = "e385cc95d3d582c328b36d1ff90feac061102b001894b555e6b465a2e0eaabbf"687
+dependencies = [688
+ "deadpool-runtime 0.3.1",689
+]690
+691
+[[package]]692
+name = "decancer"693
+version = "3.3.3"694
+source = "registry+https://github.com/rust-lang/crates.io-index"695
+checksum = "a9244323129647178bf41ac861a2cdb9d9c81b9b09d3d0d1de9cd302b33b8a1d"696
+697
+[[package]]698
+name = "delegate-display"699
+version = "3.0.1"700
+source = "registry+https://github.com/rust-lang/crates.io-index"701
+checksum = "54e3a499943fd5180aeffcb708164ffb22125ceeccb05491b5297981d37d6249"702
+dependencies = [703
+ "impartial-ord",704
+ "itoa",705
+ "macroific 3.0.1",706
+ "proc-macro2",707
+ "quote",708
+ "syn 3.0.5",709
+]710
+711
+[[package]]712
+name = "der"713
+version = "0.7.10"714
+source = "registry+https://github.com/rust-lang/crates.io-index"715
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"716
+dependencies = [717
+ "const-oid",718
+ "zeroize",719
+]720
+721
+[[package]]722
+name = "deranged"723
+version = "0.5.8"724
+source = "registry+https://github.com/rust-lang/crates.io-index"725
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"726
+727
+[[package]]728
+name = "derivative"729
+version = "2.2.0"730
+source = "registry+https://github.com/rust-lang/crates.io-index"731
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"732
+dependencies = [733
+ "proc-macro2",734
+ "quote",735
+ "syn 1.0.109",736
+]737
+738
+[[package]]739
+name = "derive_more"740
+version = "1.0.0"741
+source = "registry+https://github.com/rust-lang/crates.io-index"742
+checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"743
+dependencies = [744
+ "derive_more-impl 1.0.0",745
+]746
+747
+[[package]]748
+name = "derive_more"749
+version = "2.1.1"750
+source = "registry+https://github.com/rust-lang/crates.io-index"751
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"752
+dependencies = [753
+ "derive_more-impl 2.1.1",754
+]755
+756
+[[package]]757
+name = "derive_more-impl"758
+version = "1.0.0"759
+source = "registry+https://github.com/rust-lang/crates.io-index"760
+checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"761
+dependencies = [762
+ "proc-macro2",763
+ "quote",764
+ "syn 2.0.119",765
+]766
+767
+[[package]]768
+name = "derive_more-impl"769
+version = "2.1.1"770
+source = "registry+https://github.com/rust-lang/crates.io-index"771
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"772
+dependencies = [773
+ "proc-macro2",774
+ "quote",775
+ "rustc_version",776
+ "syn 2.0.119",777
+ "unicode-xid",778
+]779
+780
+[[package]]781
+name = "digest"782
+version = "0.10.7"783
+source = "registry+https://github.com/rust-lang/crates.io-index"784
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"785
+dependencies = [786
+ "block-buffer",787
+ "crypto-common",788
+ "subtle",789
+]790
+791
+[[package]]792
+name = "displaydoc"793
+version = "0.2.7"794
+source = "registry+https://github.com/rust-lang/crates.io-index"795
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"796
+dependencies = [797
+ "proc-macro2",798
+ "quote",799
+ "syn 3.0.5",800
+]801
+802
+[[package]]803
+name = "dunce"804
+version = "1.0.5"805
+source = "registry+https://github.com/rust-lang/crates.io-index"806
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"807
+808
+[[package]]809
+name = "ed25519"810
+version = "2.2.3"811
+source = "registry+https://github.com/rust-lang/crates.io-index"812
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"813
+dependencies = [814
+ "pkcs8",815
+ "serde",816
+ "signature",817
+]818
+819
+[[package]]820
+name = "ed25519-dalek"821
+version = "2.2.0"822
+source = "registry+https://github.com/rust-lang/crates.io-index"823
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"824
+dependencies = [825
+ "curve25519-dalek",826
+ "ed25519",827
+ "rand_core 0.6.4",828
+ "serde",829
+ "sha2",830
+ "subtle",831
+ "zeroize",832
+]833
+834
+[[package]]835
+name = "either"836
+version = "1.18.0"837
+source = "registry+https://github.com/rust-lang/crates.io-index"838
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"839
+840
+[[package]]841
+name = "emojis"842
+version = "0.8.2"843
+source = "registry+https://github.com/rust-lang/crates.io-index"844
+checksum = "0a4d5d50b0b58df5173d8ff1192b4d1422ceae5d981b30d4b6f8ed1d673a2bc4"845
+dependencies = [846
+ "phf",847
+]848
+849
+[[package]]850
+name = "encode_unicode"851
+version = "1.0.0"852
+source = "registry+https://github.com/rust-lang/crates.io-index"853
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"854
+855
+[[package]]856
+name = "equivalent"857
+version = "1.0.2"858
+source = "registry+https://github.com/rust-lang/crates.io-index"859
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"860
+861
+[[package]]862
+name = "errno"863
+version = "0.3.14"864
+source = "registry+https://github.com/rust-lang/crates.io-index"865
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"866
+dependencies = [867
+ "libc",868
+ "windows-sys 0.61.2",869
+]870
+871
+[[package]]872
+name = "event-listener"873
+version = "5.4.2"874
+source = "registry+https://github.com/rust-lang/crates.io-index"875
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"876
+dependencies = [877
+ "parking",878
+ "pin-project-lite",879
+]880
+881
+[[package]]882
+name = "event-listener-strategy"883
+version = "0.5.4"884
+source = "registry+https://github.com/rust-lang/crates.io-index"885
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"886
+dependencies = [887
+ "event-listener",888
+ "pin-project-lite",889
+]890
+891
+[[package]]892
+name = "eyeball"893
+version = "0.8.8"894
+source = "registry+https://github.com/rust-lang/crates.io-index"895
+checksum = "d93bd0ebf93d61d6332d3c09a96e97975968a44e19a64c947bde06e6baff383f"896
+dependencies = [897
+ "futures-core",898
+ "readlock",899
+ "readlock-tokio",900
+ "tokio",901
+ "tokio-util",902
+ "tracing",903
+]904
+905
+[[package]]906
+name = "eyeball-im"907
+version = "0.8.0"908
+source = "registry+https://github.com/rust-lang/crates.io-index"909
+checksum = "4790c03df183c2b46665c1a58118c04fd3e3976ec2fe16a0aa00e00c9eea7754"910
+dependencies = [911
+ "futures-core",912
+ "imbl",913
+ "tokio",914
+ "tracing",915
+]916
+917
+[[package]]918
+name = "eyeball-im-util"919
+version = "0.10.0"920
+source = "registry+https://github.com/rust-lang/crates.io-index"921
+checksum = "809fa2e8e59c25a41f5b2440aa937f1e84df6ae48b707e65f69ce259b1c44bda"922
+dependencies = [923
+ "arrayvec",924
+ "eyeball-im",925
+ "futures-core",926
+ "imbl",927
+ "pin-project-lite",928
+ "smallvec",929
+]930
+931
+[[package]]932
+name = "fallible-iterator"933
+version = "0.3.0"934
+source = "registry+https://github.com/rust-lang/crates.io-index"935
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"936
+937
+[[package]]938
+name = "fallible-streaming-iterator"939
+version = "0.1.9"940
+source = "registry+https://github.com/rust-lang/crates.io-index"941
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"942
+943
+[[package]]944
+name = "fancy_constructor"945
+version = "2.1.1"946
+source = "registry+https://github.com/rust-lang/crates.io-index"947
+checksum = "4a8211ab12c36b63c269e17873331dc1da196a3edb7a5c79015677ef972da34a"948
+dependencies = [949
+ "macroific 3.0.1",950
+ "proc-macro2",951
+ "quote",952
+ "syn 3.0.5",953
+]954
+955
+[[package]]956
+name = "fastrand"957
+version = "2.5.0"958
+source = "registry+https://github.com/rust-lang/crates.io-index"959
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"960
+961
+[[package]]962
+name = "fiat-crypto"963
+version = "0.2.9"964
+source = "registry+https://github.com/rust-lang/crates.io-index"965
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"966
+967
+[[package]]968
+name = "find-msvc-tools"969
+version = "0.1.12"970
+source = "registry+https://github.com/rust-lang/crates.io-index"971
+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"972
+973
+[[package]]974
+name = "flate2"975
+version = "1.1.10"976
+source = "registry+https://github.com/rust-lang/crates.io-index"977
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"978
+dependencies = [979
+ "crc32fast",980
+ "miniz_oxide",981
+ "zlib-rs",982
+]983
+984
+[[package]]985
+name = "fnv"986
+version = "1.0.7"987
+source = "registry+https://github.com/rust-lang/crates.io-index"988
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"989
+990
+[[package]]991
+name = "foldhash"992
+version = "0.1.5"993
+source = "registry+https://github.com/rust-lang/crates.io-index"994
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"995
+996
+[[package]]997
+name = "form_urlencoded"998
+version = "1.2.2"999
+source = "registry+https://github.com/rust-lang/crates.io-index"1000
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"1001
+dependencies = [1002
+ "percent-encoding",1003
+]1004
+1005
+[[package]]1006
+name = "fs_extra"1007
+version = "1.3.0"1008
+source = "registry+https://github.com/rust-lang/crates.io-index"1009
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"1010
+1011
+[[package]]1012
+name = "futures"1013
+version = "0.3.34"1014
+source = "registry+https://github.com/rust-lang/crates.io-index"1015
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"1016
+dependencies = [1017
+ "futures-channel",1018
+ "futures-core",1019
+ "futures-executor",1020
+ "futures-io",1021
+ "futures-sink",1022
+ "futures-task",1023
+ "futures-util",1024
+]1025
+1026
+[[package]]1027
+name = "futures-channel"1028
+version = "0.3.34"1029
+source = "registry+https://github.com/rust-lang/crates.io-index"1030
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"1031
+dependencies = [1032
+ "futures-core",1033
+ "futures-sink",1034
+]1035
+1036
+[[package]]1037
+name = "futures-core"1038
+version = "0.3.34"1039
+source = "registry+https://github.com/rust-lang/crates.io-index"1040
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"1041
+1042
+[[package]]1043
+name = "futures-executor"1044
+version = "0.3.34"1045
+source = "registry+https://github.com/rust-lang/crates.io-index"1046
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"1047
+dependencies = [1048
+ "futures-core",1049
+ "futures-task",1050
+ "futures-util",1051
+]1052
+1053
+[[package]]1054
+name = "futures-io"1055
+version = "0.3.34"1056
+source = "registry+https://github.com/rust-lang/crates.io-index"1057
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"1058
+1059
+[[package]]1060
+name = "futures-macro"1061
+version = "0.3.34"1062
+source = "registry+https://github.com/rust-lang/crates.io-index"1063
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"1064
+dependencies = [1065
+ "proc-macro2",1066
+ "quote",1067
+ "syn 3.0.5",1068
+]1069
+1070
+[[package]]1071
+name = "futures-sink"1072
+version = "0.3.34"1073
+source = "registry+https://github.com/rust-lang/crates.io-index"1074
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"1075
+1076
+[[package]]1077
+name = "futures-task"1078
+version = "0.3.34"1079
+source = "registry+https://github.com/rust-lang/crates.io-index"1080
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"1081
+1082
+[[package]]1083
+name = "futures-util"1084
+version = "0.3.34"1085
+source = "registry+https://github.com/rust-lang/crates.io-index"1086
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"1087
+dependencies = [1088
+ "futures-channel",1089
+ "futures-core",1090
+ "futures-io",1091
+ "futures-macro",1092
+ "futures-sink",1093
+ "futures-task",1094
+ "memchr",1095
+ "pin-project-lite",1096
+ "slab",1097
+]1098
+1099
+[[package]]1100
+name = "fuzzy-matcher"1101
+version = "0.3.7"1102
+source = "registry+https://github.com/rust-lang/crates.io-index"1103
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"1104
+dependencies = [1105
+ "thread_local",1106
+]1107
+1108
+[[package]]1109
+name = "generator"1110
+version = "0.8.9"1111
+source = "registry+https://github.com/rust-lang/crates.io-index"1112
+checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"1113
+dependencies = [1114
+ "cc",1115
+ "cfg-if",1116
+ "libc",1117
+ "log",1118
+ "rustversion",1119
+ "windows-link",1120
+ "windows-result",1121
+]1122
+1123
+[[package]]1124
+name = "generic-array"1125
+version = "0.14.7"1126
+source = "registry+https://github.com/rust-lang/crates.io-index"1127
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"1128
+dependencies = [1129
+ "typenum",1130
+ "version_check",1131
+]1132
+1133
+[[package]]1134
+name = "getrandom"1135
+version = "0.2.17"1136
+source = "registry+https://github.com/rust-lang/crates.io-index"1137
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"1138
+dependencies = [1139
+ "cfg-if",1140
+ "js-sys",1141
+ "libc",1142
+ "wasi",1143
+ "wasm-bindgen",1144
+]1145
+1146
+[[package]]1147
+name = "getrandom"1148
+version = "0.3.4"1149
+source = "registry+https://github.com/rust-lang/crates.io-index"1150
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"1151
+dependencies = [1152
+ "cfg-if",1153
+ "libc",1154
+ "r-efi 5.3.0",1155
+ "wasip2",1156
+]1157
+1158
+[[package]]1159
+name = "getrandom"1160
+version = "0.4.3"1161
+source = "registry+https://github.com/rust-lang/crates.io-index"1162
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"1163
+dependencies = [1164
+ "cfg-if",1165
+ "js-sys",1166
+ "libc",1167
+ "r-efi 6.0.0",1168
+ "rand_core 0.10.1",1169
+ "wasm-bindgen",1170
+]1171
+1172
+[[package]]1173
+name = "gloo-timers"1174
+version = "0.3.0"1175
+source = "registry+https://github.com/rust-lang/crates.io-index"1176
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"1177
+dependencies = [1178
+ "futures-channel",1179
+ "futures-core",1180
+ "js-sys",1181
+ "wasm-bindgen",1182
+]1183
+1184
+[[package]]1185
+name = "gloo-utils"1186
+version = "0.2.0"1187
+source = "registry+https://github.com/rust-lang/crates.io-index"1188
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"1189
+dependencies = [1190
+ "js-sys",1191
+ "serde",1192
+ "serde_json",1193
+ "wasm-bindgen",1194
+ "web-sys",1195
+]1196
+1197
+[[package]]1198
+name = "growable-bloom-filter"1199
+version = "2.1.1"1200
+source = "registry+https://github.com/rust-lang/crates.io-index"1201
+checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1"1202
+dependencies = [1203
+ "serde",1204
+ "serde_bytes",1205
+ "serde_derive",1206
+ "xxhash-rust",1207
+]1208
+1209
+[[package]]1210
+name = "h2"1211
+version = "0.4.19"1212
+source = "registry+https://github.com/rust-lang/crates.io-index"1213
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"1214
+dependencies = [1215
+ "atomic-waker",1216
+ "bytes",1217
+ "fnv",1218
+ "futures-core",1219
+ "futures-sink",1220
+ "http",1221
+ "indexmap",1222
+ "slab",1223
+ "tokio",1224
+ "tokio-util",1225
+ "tracing",1226
+]1227
+1228
+[[package]]1229
+name = "hashbrown"1230
+version = "0.15.5"1231
+source = "registry+https://github.com/rust-lang/crates.io-index"1232
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"1233
+dependencies = [1234
+ "foldhash",1235
+]1236
+1237
+[[package]]1238
+name = "hashbrown"1239
+version = "0.17.1"1240
+source = "registry+https://github.com/rust-lang/crates.io-index"1241
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"1242
+1243
+[[package]]1244
+name = "hashlink"1245
+version = "0.10.0"1246
+source = "registry+https://github.com/rust-lang/crates.io-index"1247
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"1248
+dependencies = [1249
+ "hashbrown 0.15.5",1250
+]1251
+1252
+[[package]]1253
+name = "hermit-abi"1254
+version = "0.5.3"1255
+source = "registry+https://github.com/rust-lang/crates.io-index"1256
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"1257
+1258
+[[package]]1259
+name = "hkdf"1260
+version = "0.12.4"1261
+source = "registry+https://github.com/rust-lang/crates.io-index"1262
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"1263
+dependencies = [1264
+ "hmac",1265
+]1266
+1267
+[[package]]1268
+name = "hmac"1269
+version = "0.12.1"1270
+source = "registry+https://github.com/rust-lang/crates.io-index"1271
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"1272
+dependencies = [1273
+ "digest",1274
+]1275
+1276
+[[package]]1277
+name = "html5ever"1278
+version = "0.39.0"1279
+source = "registry+https://github.com/rust-lang/crates.io-index"1280
+checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"1281
+dependencies = [1282
+ "log",1283
+ "markup5ever",1284
+]1285
+1286
+[[package]]1287
+name = "http"1288
+version = "1.5.0"1289
+source = "registry+https://github.com/rust-lang/crates.io-index"1290
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"1291
+dependencies = [1292
+ "bytes",1293
+ "itoa",1294
+]1295
+1296
+[[package]]1297
+name = "http-body"1298
+version = "1.1.0"1299
+source = "registry+https://github.com/rust-lang/crates.io-index"1300
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"1301
+dependencies = [1302
+ "bytes",1303
+ "http",1304
+]1305
+1306
+[[package]]1307
+name = "http-body-util"1308
+version = "0.1.5"1309
+source = "registry+https://github.com/rust-lang/crates.io-index"1310
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"1311
+dependencies = [1312
+ "bytes",1313
+ "futures-core",1314
+ "http",1315
+ "http-body",1316
+ "pin-project-lite",1317
+]1318
+1319
+[[package]]1320
+name = "httparse"1321
+version = "1.10.1"1322
+source = "registry+https://github.com/rust-lang/crates.io-index"1323
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"1324
+1325
+[[package]]1326
+name = "httpdate"1327
+version = "1.0.3"1328
+source = "registry+https://github.com/rust-lang/crates.io-index"1329
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"1330
+1331
+[[package]]1332
+name = "hyper"1333
+version = "1.11.1"1334
+source = "registry+https://github.com/rust-lang/crates.io-index"1335
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"1336
+dependencies = [1337
+ "atomic-waker",1338
+ "bytes",1339
+ "futures-channel",1340
+ "futures-core",1341
+ "h2",1342
+ "http",1343
+ "http-body",1344
+ "httparse",1345
+ "httpdate",1346
+ "itoa",1347
+ "pin-project-lite",1348
+ "smallvec",1349
+ "tokio",1350
+ "want",1351
+]1352
+1353
+[[package]]1354
+name = "hyper-rustls"1355
+version = "0.27.9"1356
+source = "registry+https://github.com/rust-lang/crates.io-index"1357
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"1358
+dependencies = [1359
+ "http",1360
+ "hyper",1361
+ "hyper-util",1362
+ "rustls",1363
+ "tokio",1364
+ "tokio-rustls",1365
+ "tower-service",1366
+]1367
+1368
+[[package]]1369
+name = "hyper-util"1370
+version = "0.1.20"1371
+source = "registry+https://github.com/rust-lang/crates.io-index"1372
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"1373
+dependencies = [1374
+ "base64",1375
+ "bytes",1376
+ "futures-channel",1377
+ "futures-util",1378
+ "http",1379
+ "http-body",1380
+ "hyper",1381
+ "ipnet",1382
+ "libc",1383
+ "percent-encoding",1384
+ "pin-project-lite",1385
+ "socket2",1386
+ "tokio",1387
+ "tower-service",1388
+ "tracing",1389
+]1390
+1391
+[[package]]1392
+name = "iana-time-zone"1393
+version = "0.1.65"1394
+source = "registry+https://github.com/rust-lang/crates.io-index"1395
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"1396
+dependencies = [1397
+ "android_system_properties",1398
+ "core-foundation-sys",1399
+ "iana-time-zone-haiku",1400
+ "js-sys",1401
+ "log",1402
+ "wasm-bindgen",1403
+ "windows-core",1404
+]1405
+1406
+[[package]]1407
+name = "iana-time-zone-haiku"1408
+version = "0.1.2"1409
+source = "registry+https://github.com/rust-lang/crates.io-index"1410
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"1411
+dependencies = [1412
+ "cc",1413
+]1414
+1415
+[[package]]1416
+name = "icu_collections"1417
+version = "2.3.0"1418
+source = "registry+https://github.com/rust-lang/crates.io-index"1419
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"1420
+dependencies = [1421
+ "displaydoc",1422
+ "potential_utf",1423
+ "utf8_iter",1424
+ "yoke",1425
+ "zerofrom",1426
+ "zerovec",1427
+]1428
+1429
+[[package]]1430
+name = "icu_locale_core"1431
+version = "2.3.0"1432
+source = "registry+https://github.com/rust-lang/crates.io-index"1433
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"1434
+dependencies = [1435
+ "displaydoc",1436
+ "litemap",1437
+ "tinystr",1438
+ "writeable",1439
+ "zerovec",1440
+]1441
+1442
+[[package]]1443
+name = "icu_normalizer"1444
+version = "2.3.0"1445
+source = "registry+https://github.com/rust-lang/crates.io-index"1446
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"1447
+dependencies = [1448
+ "icu_collections",1449
+ "icu_normalizer_data",1450
+ "icu_properties",1451
+ "icu_provider",1452
+ "smallvec",1453
+ "zerovec",1454
+]1455
+1456
+[[package]]1457
+name = "icu_normalizer_data"1458
+version = "2.3.0"1459
+source = "registry+https://github.com/rust-lang/crates.io-index"1460
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"1461
+1462
+[[package]]1463
+name = "icu_properties"1464
+version = "2.3.0"1465
+source = "registry+https://github.com/rust-lang/crates.io-index"1466
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"1467
+dependencies = [1468
+ "displaydoc",1469
+ "icu_collections",1470
+ "icu_locale_core",1471
+ "icu_properties_data",1472
+ "icu_provider",1473
+ "zerotrie",1474
+ "zerovec",1475
+]1476
+1477
+[[package]]1478
+name = "icu_properties_data"1479
+version = "2.3.0"1480
+source = "registry+https://github.com/rust-lang/crates.io-index"1481
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"1482
+1483
+[[package]]1484
+name = "icu_provider"1485
+version = "2.3.1"1486
+source = "registry+https://github.com/rust-lang/crates.io-index"1487
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"1488
+dependencies = [1489
+ "displaydoc",1490
+ "icu_locale_core",1491
+ "writeable",1492
+ "yoke",1493
+ "zerofrom",1494
+ "zerotrie",1495
+ "zerovec",1496
+]1497
+1498
+[[package]]1499
+name = "idna"1500
+version = "1.1.0"1501
+source = "registry+https://github.com/rust-lang/crates.io-index"1502
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"1503
+dependencies = [1504
+ "idna_adapter",1505
+ "smallvec",1506
+ "utf8_iter",1507
+]1508
+1509
+[[package]]1510
+name = "idna_adapter"1511
+version = "1.2.2"1512
+source = "registry+https://github.com/rust-lang/crates.io-index"1513
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"1514
+dependencies = [1515
+ "icu_normalizer",1516
+ "icu_properties",1517
+]1518
+1519
+[[package]]1520
+name = "imbl"1521
+version = "6.1.0"1522
+source = "registry+https://github.com/rust-lang/crates.io-index"1523
+checksum = "0fade8ae6828627ad1fa094a891eccfb25150b383047190a3648d66d06186501"1524
+dependencies = [1525
+ "archery",1526
+ "bitmaps",1527
+ "imbl-sized-chunks",1528
+ "rand_core 0.9.5",1529
+ "rand_xoshiro",1530
+ "serde",1531
+ "version_check",1532
+]1533
+1534
+[[package]]1535
+name = "imbl-sized-chunks"1536
+version = "0.1.3"1537
+source = "registry+https://github.com/rust-lang/crates.io-index"1538
+checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d"1539
+dependencies = [1540
+ "bitmaps",1541
+]1542
+1543
+[[package]]1544
+name = "impartial-ord"1545
+version = "2.0.0"1546
+source = "registry+https://github.com/rust-lang/crates.io-index"1547
+checksum = "bc680e03d1a08bdc1c01b60e0c39723430ced63d8cabd93cee0449b6ba916a25"1548
+dependencies = [1549
+ "proc-macro2",1550
+ "quote",1551
+ "syn 3.0.5",1552
+]1553
+1554
+[[package]]1555
+name = "include_dir"1556
+version = "0.7.4"1557
+source = "registry+https://github.com/rust-lang/crates.io-index"1558
+checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"1559
+dependencies = [1560
+ "include_dir_macros",1561
+]1562
+1563
+[[package]]1564
+name = "include_dir_macros"1565
+version = "0.7.4"1566
+source = "registry+https://github.com/rust-lang/crates.io-index"1567
+checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"1568
+dependencies = [1569
+ "proc-macro2",1570
+ "quote",1571
+]1572
+1573
+[[package]]1574
+name = "indexmap"1575
+version = "2.14.2"1576
+source = "registry+https://github.com/rust-lang/crates.io-index"1577
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"1578
+dependencies = [1579
+ "equivalent",1580
+ "hashbrown 0.17.1",1581
+ "serde",1582
+ "serde_core",1583
+]1584
+1585
+[[package]]1586
+name = "inout"1587
+version = "0.1.4"1588
+source = "registry+https://github.com/rust-lang/crates.io-index"1589
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"1590
+dependencies = [1591
+ "block-padding",1592
+ "generic-array",1593
+]1594
+1595
+[[package]]1596
+name = "insta"1597
+version = "1.48.0"1598
+source = "registry+https://github.com/rust-lang/crates.io-index"1599
+checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"1600
+dependencies = [1601
+ "console",1602
+ "once_cell",1603
+ "pest",1604
+ "pest_derive",1605
+ "serde",1606
+ "similar",1607
+ "tempfile",1608
+]1609
+1610
+[[package]]1611
+name = "ipnet"1612
+version = "2.12.2"1613
+source = "registry+https://github.com/rust-lang/crates.io-index"1614
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"1615
+1616
+[[package]]1617
+name = "itertools"1618
+version = "0.10.5"1619
+source = "registry+https://github.com/rust-lang/crates.io-index"1620
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"1621
+dependencies = [1622
+ "either",1623
+]1624
+1625
+[[package]]1626
+name = "itertools"1627
+version = "0.14.0"1628
+source = "registry+https://github.com/rust-lang/crates.io-index"1629
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"1630
+dependencies = [1631
+ "either",1632
+]1633
+1634
+[[package]]1635
+name = "itoa"1636
+version = "1.0.18"1637
+source = "registry+https://github.com/rust-lang/crates.io-index"1638
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"1639
+1640
+[[package]]1641
+name = "jni"1642
+version = "0.22.4"1643
+source = "registry+https://github.com/rust-lang/crates.io-index"1644
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"1645
+dependencies = [1646
+ "cfg-if",1647
+ "combine",1648
+ "jni-macros",1649
+ "jni-sys",1650
+ "log",1651
+ "simd_cesu8",1652
+ "thiserror 2.0.20",1653
+ "walkdir",1654
+ "windows-link",1655
+]1656
+1657
+[[package]]1658
+name = "jni-macros"1659
+version = "0.22.4"1660
+source = "registry+https://github.com/rust-lang/crates.io-index"1661
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"1662
+dependencies = [1663
+ "proc-macro2",1664
+ "quote",1665
+ "rustc_version",1666
+ "simd_cesu8",1667
+ "syn 2.0.119",1668
+]1669
+1670
+[[package]]1671
+name = "jni-sys"1672
+version = "0.4.1"1673
+source = "registry+https://github.com/rust-lang/crates.io-index"1674
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"1675
+dependencies = [1676
+ "jni-sys-macros",1677
+]1678
+1679
+[[package]]1680
+name = "jni-sys-macros"1681
+version = "0.4.1"1682
+source = "registry+https://github.com/rust-lang/crates.io-index"1683
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"1684
+dependencies = [1685
+ "quote",1686
+ "syn 2.0.119",1687
+]1688
+1689
+[[package]]1690
+name = "jobserver"1691
+version = "0.1.35"1692
+source = "registry+https://github.com/rust-lang/crates.io-index"1693
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"1694
+dependencies = [1695
+ "getrandom 0.4.3",1696
+ "libc",1697
+]1698
+1699
+[[package]]1700
+name = "js-sys"1701
+version = "0.3.105"1702
+source = "registry+https://github.com/rust-lang/crates.io-index"1703
+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"1704
+dependencies = [1705
+ "cfg-if",1706
+ "futures-util",1707
+ "wasm-bindgen",1708
+]1709
+1710
+[[package]]1711
+name = "js_int"1712
+version = "0.2.2"1713
+source = "registry+https://github.com/rust-lang/crates.io-index"1714
+checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a"1715
+dependencies = [1716
+ "serde",1717
+]1718
+1719
+[[package]]1720
+name = "js_option"1721
+version = "0.2.0"1722
+source = "registry+https://github.com/rust-lang/crates.io-index"1723
+checksum = "c7dd3e281add16813cf673bf74a32249b0aa0d1c8117519a17b3ada5e8552b3c"1724
+dependencies = [1725
+ "serde_core",1726
+]1727
+1728
+[[package]]1729
+name = "konst"1730
+version = "0.4.3"1731
+source = "registry+https://github.com/rust-lang/crates.io-index"1732
+checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d"1733
+dependencies = [1734
+ "const_panic",1735
+ "typewit",1736
+]1737
+1738
+[[package]]1739
+name = "language-tags"1740
+version = "0.3.2"1741
+source = "registry+https://github.com/rust-lang/crates.io-index"1742
+checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"1743
+1744
+[[package]]1745
+name = "lazy_static"1746
+version = "1.5.0"1747
+source = "registry+https://github.com/rust-lang/crates.io-index"1748
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"1749
+1750
+[[package]]1751
+name = "libc"1752
+version = "0.2.189"1753
+source = "registry+https://github.com/rust-lang/crates.io-index"1754
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"1755
+1756
+[[package]]1757
+name = "libm"1758
+version = "0.2.16"1759
+source = "registry+https://github.com/rust-lang/crates.io-index"1760
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"1761
+1762
+[[package]]1763
+name = "libsqlite3-sys"1764
+version = "0.35.0"1765
+source = "registry+https://github.com/rust-lang/crates.io-index"1766
+checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"1767
+dependencies = [1768
+ "cc",1769
+ "pkg-config",1770
+ "vcpkg",1771
+]1772
+1773
+[[package]]1774
+name = "linux-raw-sys"1775
+version = "0.12.1"1776
+source = "registry+https://github.com/rust-lang/crates.io-index"1777
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"1778
+1779
+[[package]]1780
+name = "litemap"1781
+version = "0.8.3"1782
+source = "registry+https://github.com/rust-lang/crates.io-index"1783
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"1784
+1785
+[[package]]1786
+name = "lock_api"1787
+version = "0.4.14"1788
+source = "registry+https://github.com/rust-lang/crates.io-index"1789
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"1790
+dependencies = [1791
+ "scopeguard",1792
+]1793
+1794
+[[package]]1795
+name = "log"1796
+version = "0.4.34"1797
+source = "registry+https://github.com/rust-lang/crates.io-index"1798
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"1799
+1800
+[[package]]1801
+name = "loom"1802
+version = "0.7.2"1803
+source = "registry+https://github.com/rust-lang/crates.io-index"1804
+checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"1805
+dependencies = [1806
+ "cfg-if",1807
+ "generator",1808
+ "scoped-tls",1809
+ "tracing",1810
+ "tracing-subscriber",1811
+]1812
+1813
+[[package]]1814
+name = "lru-slab"1815
+version = "0.1.2"1816
+source = "registry+https://github.com/rust-lang/crates.io-index"1817
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"1818
+1819
+[[package]]1820
+name = "macltok"1821
+version = "0.1.0"1822
+dependencies = [1823
+ "futures-util",1824
+ "http",1825
+ "matrix-sdk",1826
+ "matrix-sdk-crypto",1827
+ "matrix-sdk-test",1828
+ "matrix-sdk-ui",1829
+ "mime",1830
+ "ruma",1831
+ "serde",1832
+ "serde_json",1833
+ "thiserror 2.0.20",1834
+ "tokio",1835
+ "tracing",1836
+ "url",1837
+ "wiremock",1838
+]1839
+1840
+[[package]]1841
+name = "macroific"1842
+version = "2.0.0"1843
+source = "registry+https://github.com/rust-lang/crates.io-index"1844
+checksum = "89f276537b4b8f981bf1c13d79470980f71134b7bdcc5e6e911e910e556b0285"1845
+dependencies = [1846
+ "macroific_attr_parse 2.0.0",1847
+ "macroific_core 2.0.0",1848
+ "macroific_macro 2.0.0",1849
+]1850
+1851
+[[package]]1852
+name = "macroific"1853
+version = "3.0.1"1854
+source = "registry+https://github.com/rust-lang/crates.io-index"1855
+checksum = "c437abb3bf40939b00d1dff858ed2050d948bc230dee39546ed16795fcf10ec2"1856
+dependencies = [1857
+ "macroific_attr_parse 3.0.1",1858
+ "macroific_core 3.0.1",1859
+ "macroific_macro 3.0.1",1860
+]1861
+1862
+[[package]]1863
+name = "macroific_attr_parse"1864
+version = "2.0.0"1865
+source = "registry+https://github.com/rust-lang/crates.io-index"1866
+checksum = "ad4023761b45fcd36abed8fb7ae6a80456b0a38102d55e89a57d9a594a236be9"1867
+dependencies = [1868
+ "proc-macro2",1869
+ "quote",1870
+ "sealed 0.6.0",1871
+ "syn 2.0.119",1872
+]1873
+1874
+[[package]]1875
+name = "macroific_attr_parse"1876
+version = "3.0.1"1877
+source = "registry+https://github.com/rust-lang/crates.io-index"1878
+checksum = "aeb7a82ec1aa16094da719be18615d79cb2574b037358e4e6c6d96eedcff3a93"1879
+dependencies = [1880
+ "proc-macro2",1881
+ "quote",1882
+ "sealed 0.7.0",1883
+ "syn 3.0.5",1884
+]1885
+1886
+[[package]]1887
+name = "macroific_core"1888
+version = "2.0.0"1889
+source = "registry+https://github.com/rust-lang/crates.io-index"1890
+checksum = "d0a7594d3c14916fa55bef7e9d18c5daa9ed410dd37504251e4b75bbdeec33e3"1891
+dependencies = [1892
+ "proc-macro2",1893
+ "quote",1894
+ "sealed 0.6.0",1895
+ "syn 2.0.119",1896
+]1897
+1898
+[[package]]1899
+name = "macroific_core"1900
+version = "3.0.1"1901
+source = "registry+https://github.com/rust-lang/crates.io-index"1902
+checksum = "26c8e789484d2fa216eee9d5e0565ba80ccda389bcf0e111143a7b8f4792336e"1903
+dependencies = [1904
+ "proc-macro2",1905
+ "quote",1906
+ "sealed 0.7.0",1907
+ "syn 3.0.5",1908
+]1909
+1910
+[[package]]1911
+name = "macroific_macro"1912
+version = "2.0.0"1913
+source = "registry+https://github.com/rust-lang/crates.io-index"1914
+checksum = "4da6f2ed796261b0a74e2b52b42c693bb6dee1effba3a482c49592659f824b3b"1915
+dependencies = [1916
+ "macroific_attr_parse 2.0.0",1917
+ "macroific_core 2.0.0",1918
+ "proc-macro2",1919
+ "quote",1920
+ "syn 2.0.119",1921
+]1922
+1923
+[[package]]1924
+name = "macroific_macro"1925
+version = "3.0.1"1926
+source = "registry+https://github.com/rust-lang/crates.io-index"1927
+checksum = "e30d63cb102648965753f0d47a6ab171b1cbb1fb5c363f4784ea986782fc7b77"1928
+dependencies = [1929
+ "macroific_attr_parse 3.0.1",1930
+ "macroific_core 3.0.1",1931
+ "proc-macro2",1932
+ "quote",1933
+ "syn 3.0.5",1934
+]1935
+1936
+[[package]]1937
+name = "maplit"1938
+version = "1.0.2"1939
+source = "registry+https://github.com/rust-lang/crates.io-index"1940
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"1941
+1942
+[[package]]1943
+name = "markup5ever"1944
+version = "0.39.0"1945
+source = "registry+https://github.com/rust-lang/crates.io-index"1946
+checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"1947
+dependencies = [1948
+ "log",1949
+ "tendril",1950
+ "web_atoms",1951
+]1952
+1953
+[[package]]1954
+name = "matchers"1955
+version = "0.2.0"1956
+source = "registry+https://github.com/rust-lang/crates.io-index"1957
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"1958
+dependencies = [1959
+ "regex-automata",1960
+]1961
+1962
+[[package]]1963
+name = "matrix-pickle"1964
+version = "0.2.3"1965
+source = "registry+https://github.com/rust-lang/crates.io-index"1966
+checksum = "b3d65d46b7379dd0afa4a42f9b2269821d31afdee0111b5e0d74e3bee03553a0"1967
+dependencies = [1968
+ "matrix-pickle-derive",1969
+ "thiserror 2.0.20",1970
+]1971
+1972
+[[package]]1973
+name = "matrix-pickle-derive"1974
+version = "0.2.3"1975
+source = "registry+https://github.com/rust-lang/crates.io-index"1976
+checksum = "414b5e4c34009f2bc3fe35dd018f25755ca38858096574841c7332f99e2c7e77"1977
+dependencies = [1978
+ "proc-macro-crate",1979
+ "proc-macro2",1980
+ "quote",1981
+ "syn 2.0.119",1982
+]1983
+1984
+[[package]]1985
+name = "matrix-sdk"1986
+version = "0.18.0"1987
+source = "registry+https://github.com/rust-lang/crates.io-index"1988
+checksum = "7083d580527511ac5d9369e03b9f2b20902e76949f1b3964051f978e4d3756ae"1989
+dependencies = [1990
+ "anymap2",1991
+ "aquamarine",1992
+ "as_variant",1993
+ "assert_matches2",1994
+ "async-channel",1995
+ "async-once-cell",1996
+ "async-stream",1997
+ "async-trait",1998
+ "backon",1999
+ "bytes",2000
+ "bytesize",2001
+ "cfg-if",2002
+ "event-listener",2003
+ "eyeball",2004
+ "eyeball-im",2005
+ "futures-core",2006
+ "futures-util",2007
+ "gloo-timers",2008
+ "http",2009
+ "imbl",2010
+ "indexmap",2011
+ "itertools 0.14.0",2012
+ "js_int",2013
+ "language-tags",2014
+ "matrix-sdk-base",2015
+ "matrix-sdk-common",2016
+ "matrix-sdk-indexeddb",2017
+ "matrix-sdk-sqlite",2018
+ "matrix-sdk-test",2019
+ "mime",2020
+ "mime2ext",2021
+ "oauth2",2022
+ "oauth2-reqwest",2023
+ "percent-encoding",2024
+ "pin-project-lite",2025
+ "reqwest",2026
+ "ruma",2027
+ "rustls",2028
+ "rustls-native-certs",2029
+ "rustls-pki-types",2030
+ "serde",2031
+ "serde_html_form",2032
+ "serde_json",2033
+ "sha2",2034
+ "tempfile",2035
+ "thiserror 2.0.20",2036
+ "tokio",2037
+ "tokio-stream",2038
+ "tokio-util",2039
+ "tracing",2040
+ "url",2041
+ "urlencoding",2042
+ "vodozemac",2043
+ "webpki-roots",2044
+ "wiremock",2045
+ "zeroize",2046
+]2047
+2048
+[[package]]2049
+name = "matrix-sdk-base"2050
+version = "0.18.0"2051
+source = "registry+https://github.com/rust-lang/crates.io-index"2052
+checksum = "e09a917eb1f7643d9d9a06b2f131e2ceb39df6910a7b830dac18bfd6db37a1f5"2053
+dependencies = [2054
+ "as_variant",2055
+ "assert_matches",2056
+ "assert_matches2",2057
+ "async-trait",2058
+ "bitflags",2059
+ "decancer",2060
+ "eyeball",2061
+ "eyeball-im",2062
+ "futures-util",2063
+ "growable-bloom-filter",2064
+ "http",2065
+ "matrix-sdk-common",2066
+ "matrix-sdk-crypto",2067
+ "matrix-sdk-store-encryption",2068
+ "matrix-sdk-test",2069
+ "regex",2070
+ "ruma",2071
+ "serde",2072
+ "serde_json",2073
+ "thiserror 2.0.20",2074
+ "tokio",2075
+ "tracing",2076
+ "unicode-normalization",2077
+]2078
+2079
+[[package]]2080
+name = "matrix-sdk-common"2081
+version = "0.18.0"2082
+source = "registry+https://github.com/rust-lang/crates.io-index"2083
+checksum = "2b9d1e0fee0f090180ef9457034adc547e80de9e6e3eb2e63aaac68a8476f836"2084
+dependencies = [2085
+ "eyeball-im",2086
+ "futures-core",2087
+ "futures-executor",2088
+ "futures-util",2089
+ "gloo-timers",2090
+ "imbl",2091
+ "ruma",2092
+ "serde",2093
+ "serde_json",2094
+ "thiserror 2.0.20",2095
+ "tokio",2096
+ "tracing",2097
+ "tracing-subscriber",2098
+ "wasm-bindgen",2099
+ "wasm-bindgen-futures",2100
+ "web-sys",2101
+]2102
+2103
+[[package]]2104
+name = "matrix-sdk-crypto"2105
+version = "0.18.0"2106
+source = "registry+https://github.com/rust-lang/crates.io-index"2107
+checksum = "c54afd2a326f51c13a6ad44ec86315a688fffeb3f1e287fb343e0e1836a3bdaf"2108
+dependencies = [2109
+ "aes",2110
+ "aquamarine",2111
+ "as_variant",2112
+ "async-trait",2113
+ "bs58",2114
+ "byteorder",2115
+ "cfg-if",2116
+ "ctr",2117
+ "eyeball",2118
+ "futures-core",2119
+ "futures-util",2120
+ "hkdf",2121
+ "hmac",2122
+ "itertools 0.14.0",2123
+ "js_option",2124
+ "matrix-sdk-common",2125
+ "matrix-sdk-test",2126
+ "pbkdf2",2127
+ "rand 0.10.2",2128
+ "rmp-serde",2129
+ "ruma",2130
+ "serde",2131
+ "serde_json",2132
+ "sha2",2133
+ "subtle",2134
+ "thiserror 2.0.20",2135
+ "time",2136
+ "tokio",2137
+ "tokio-stream",2138
+ "tracing",2139
+ "ulid",2140
+ "url",2141
+ "vodozemac",2142
+ "zeroize",2143
+]2144
+2145
+[[package]]2146
+name = "matrix-sdk-indexeddb"2147
+version = "0.18.0"2148
+source = "registry+https://github.com/rust-lang/crates.io-index"2149
+checksum = "fef37395fffb7c916f7109ab0d16d8ca599403dd8164d08c0d966176b66ede47"2150
+dependencies = [2151
+ "async-trait",2152
+ "base64",2153
+ "futures-util",2154
+ "getrandom 0.4.3",2155
+ "gloo-utils",2156
+ "hkdf",2157
+ "js-sys",2158
+ "matrix-sdk-base",2159
+ "matrix-sdk-crypto",2160
+ "matrix-sdk-store-encryption",2161
+ "matrix_indexed_db_futures",2162
+ "rmp-serde",2163
+ "ruma",2164
+ "serde",2165
+ "serde-wasm-bindgen",2166
+ "serde_json",2167
+ "sha2",2168
+ "thiserror 2.0.20",2169
+ "tokio",2170
+ "tracing",2171
+ "uuid",2172
+ "wasm-bindgen",2173
+ "web-sys",2174
+ "zeroize",2175
+]2176
+2177
+[[package]]2178
+name = "matrix-sdk-sqlite"2179
+version = "0.18.0"2180
+source = "registry+https://github.com/rust-lang/crates.io-index"2181
+checksum = "a49133429271005745f8d5a05362d2ba6567274777250236799927ca30dc0670"2182
+dependencies = [2183
+ "as_variant",2184
+ "async-trait",2185
+ "deadpool 0.13.1",2186
+ "deadpool-sync",2187
+ "itertools 0.14.0",2188
+ "matrix-sdk-base",2189
+ "matrix-sdk-crypto",2190
+ "matrix-sdk-store-encryption",2191
+ "num_cpus",2192
+ "rmp-serde",2193
+ "ruma",2194
+ "rusqlite",2195
+ "serde",2196
+ "serde_json",2197
+ "serde_path_to_error",2198
+ "thiserror 2.0.20",2199
+ "tokio",2200
+ "tracing",2201
+ "vodozemac",2202
+ "zeroize",2203
+]2204
+2205
+[[package]]2206
+name = "matrix-sdk-store-encryption"2207
+version = "0.18.0"2208
+source = "registry+https://github.com/rust-lang/crates.io-index"2209
+checksum = "2f48f304e553fb6200b1d7d1f77a88fd182076d4b25624e9dbfa42d6a37de35e"2210
+dependencies = [2211
+ "base64",2212
+ "blake3",2213
+ "chacha20poly1305",2214
+ "getrandom 0.2.17",2215
+ "getrandom 0.4.3",2216
+ "hmac",2217
+ "pbkdf2",2218
+ "rand 0.10.2",2219
+ "rmp-serde",2220
+ "serde",2221
+ "serde_json",2222
+ "sha2",2223
+ "thiserror 2.0.20",2224
+ "zeroize",2225
+]2226
+2227
+[[package]]2228
+name = "matrix-sdk-test"2229
+version = "0.18.0"2230
+source = "registry+https://github.com/rust-lang/crates.io-index"2231
+checksum = "8023d4315fff35598c2d4a955920c1273c6aaab4dcd9ed405238a882581649bf"2232
+dependencies = [2233
+ "as_variant",2234
+ "ctor",2235
+ "getrandom 0.4.3",2236
+ "http",2237
+ "insta",2238
+ "matrix-sdk-common",2239
+ "matrix-sdk-test-macros",2240
+ "ruma",2241
+ "serde",2242
+ "serde_json",2243
+ "sha2",2244
+ "tokio",2245
+ "tracing-subscriber",2246
+ "vodozemac",2247
+ "wasm-bindgen-test",2248
+ "wiremock",2249
+]2250
+2251
+[[package]]2252
+name = "matrix-sdk-test-macros"2253
+version = "0.18.0"2254
+source = "registry+https://github.com/rust-lang/crates.io-index"2255
+checksum = "d97d6a0e5b1401c530b3c5d18cb98257d255d396bacf3903beda6718a2f4c4c5"2256
+dependencies = [2257
+ "quote",2258
+ "syn 2.0.119",2259
+]2260
+2261
+[[package]]2262
+name = "matrix-sdk-ui"2263
+version = "0.18.0"2264
+source = "registry+https://github.com/rust-lang/crates.io-index"2265
+checksum = "6d1f2b8a8cf57164d7cee4eb61c0b9f074b2e11d766a9391b7db0b9f2cbd83d8"2266
+dependencies = [2267
+ "as_variant",2268
+ "async-rx",2269
+ "async-stream",2270
+ "async_cell",2271
+ "bitflags",2272
+ "chrono",2273
+ "emojis",2274
+ "eyeball",2275
+ "eyeball-im",2276
+ "eyeball-im-util",2277
+ "futures-core",2278
+ "futures-util",2279
+ "fuzzy-matcher",2280
+ "growable-bloom-filter",2281
+ "imbl",2282
+ "indexmap",2283
+ "itertools 0.14.0",2284
+ "matrix-sdk",2285
+ "matrix-sdk-base",2286
+ "matrix-sdk-common",2287
+ "mime",2288
+ "pin-project-lite",2289
+ "ruma",2290
+ "serde",2291
+ "serde_json",2292
+ "thiserror 2.0.20",2293
+ "tokio",2294
+ "tokio-stream",2295
+ "tracing",2296
+ "unicode-normalization",2297
+ "unicode-segmentation",2298
+]2299
+2300
+[[package]]2301
+name = "matrix_indexed_db_futures"2302
+version = "0.7.0"2303
+source = "registry+https://github.com/rust-lang/crates.io-index"2304
+checksum = "245ff6a224b4df7b0c90dda2dd5a6eb46112708d49e8bdd8b007fccb09fea8e4"2305
+dependencies = [2306
+ "accessory",2307
+ "cfg-if",2308
+ "delegate-display",2309
+ "derive_more 2.1.1",2310
+ "fancy_constructor",2311
+ "futures-core",2312
+ "js-sys",2313
+ "matrix_indexed_db_futures_macros_internal",2314
+ "sealed 0.6.0",2315
+ "serde",2316
+ "serde-wasm-bindgen",2317
+ "smallvec",2318
+ "thiserror 2.0.20",2319
+ "tokio",2320
+ "wasm-bindgen",2321
+ "wasm-bindgen-futures",2322
+ "wasm_evt_listener",2323
+ "web-sys",2324
+ "web-time",2325
+]2326
+2327
+[[package]]2328
+name = "matrix_indexed_db_futures_macros_internal"2329
+version = "1.0.0"2330
+source = "registry+https://github.com/rust-lang/crates.io-index"2331
+checksum = "b428aee5c0fe9e5babd29e99d289b7f64718c444989aac0442d1fd6d3e3f66d1"2332
+dependencies = [2333
+ "macroific 2.0.0",2334
+ "proc-macro2",2335
+ "quote",2336
+ "syn 2.0.119",2337
+]2338
+2339
+[[package]]2340
+name = "memchr"2341
+version = "2.8.3"2342
+source = "registry+https://github.com/rust-lang/crates.io-index"2343
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"2344
+2345
+[[package]]2346
+name = "mime"2347
+version = "0.3.17"2348
+source = "registry+https://github.com/rust-lang/crates.io-index"2349
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"2350
+2351
+[[package]]2352
+name = "mime2ext"2353
+version = "0.1.54"2354
+source = "registry+https://github.com/rust-lang/crates.io-index"2355
+checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc"2356
+2357
+[[package]]2358
+name = "minicov"2359
+version = "0.3.8"2360
+source = "registry+https://github.com/rust-lang/crates.io-index"2361
+checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"2362
+dependencies = [2363
+ "cc",2364
+ "walkdir",2365
+]2366
+2367
+[[package]]2368
+name = "miniz_oxide"2369
+version = "0.9.1"2370
+source = "registry+https://github.com/rust-lang/crates.io-index"2371
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"2372
+dependencies = [2373
+ "adler2",2374
+ "simd-adler32",2375
+]2376
+2377
+[[package]]2378
+name = "mio"2379
+version = "1.2.3"2380
+source = "registry+https://github.com/rust-lang/crates.io-index"2381
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"2382
+dependencies = [2383
+ "libc",2384
+ "wasi",2385
+ "windows-sys 0.61.2",2386
+]2387
+2388
+[[package]]2389
+name = "new_debug_unreachable"2390
+version = "1.0.6"2391
+source = "registry+https://github.com/rust-lang/crates.io-index"2392
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"2393
+2394
+[[package]]2395
+name = "nu-ansi-term"2396
+version = "0.50.3"2397
+source = "registry+https://github.com/rust-lang/crates.io-index"2398
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"2399
+dependencies = [2400
+ "windows-sys 0.61.2",2401
+]2402
+2403
+[[package]]2404
+name = "num-conv"2405
+version = "0.2.2"2406
+source = "registry+https://github.com/rust-lang/crates.io-index"2407
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"2408
+2409
+[[package]]2410
+name = "num-traits"2411
+version = "0.2.19"2412
+source = "registry+https://github.com/rust-lang/crates.io-index"2413
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"2414
+dependencies = [2415
+ "autocfg",2416
+ "libm",2417
+]2418
+2419
+[[package]]2420
+name = "num_cpus"2421
+version = "1.17.0"2422
+source = "registry+https://github.com/rust-lang/crates.io-index"2423
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"2424
+dependencies = [2425
+ "hermit-abi",2426
+ "libc",2427
+]2428
+2429
+[[package]]2430
+name = "oauth2"2431
+version = "5.0.0"2432
+source = "registry+https://github.com/rust-lang/crates.io-index"2433
+checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"2434
+dependencies = [2435
+ "base64",2436
+ "chrono",2437
+ "getrandom 0.2.17",2438
+ "http",2439
+ "rand 0.8.8",2440
+ "serde",2441
+ "serde_json",2442
+ "serde_path_to_error",2443
+ "sha2",2444
+ "thiserror 1.0.69",2445
+ "url",2446
+]2447
+2448
+[[package]]2449
+name = "oauth2-reqwest"2450
+version = "0.1.0-alpha.3"2451
+source = "registry+https://github.com/rust-lang/crates.io-index"2452
+checksum = "234fb5c965bbce983ee5de636a7a51d6a3223da8067ea02f9ab2d2d78ac08be2"2453
+dependencies = [2454
+ "oauth2",2455
+ "reqwest",2456
+]2457
+2458
+[[package]]2459
+name = "once_cell"2460
+version = "1.21.4"2461
+source = "registry+https://github.com/rust-lang/crates.io-index"2462
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"2463
+2464
+[[package]]2465
+name = "oorandom"2466
+version = "11.1.5"2467
+source = "registry+https://github.com/rust-lang/crates.io-index"2468
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"2469
+2470
+[[package]]2471
+name = "opaque-debug"2472
+version = "0.3.1"2473
+source = "registry+https://github.com/rust-lang/crates.io-index"2474
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"2475
+2476
+[[package]]2477
+name = "openssl-probe"2478
+version = "0.2.1"2479
+source = "registry+https://github.com/rust-lang/crates.io-index"2480
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"2481
+2482
+[[package]]2483
+name = "parking"2484
+version = "2.2.1"2485
+source = "registry+https://github.com/rust-lang/crates.io-index"2486
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"2487
+2488
+[[package]]2489
+name = "parking_lot"2490
+version = "0.12.5"2491
+source = "registry+https://github.com/rust-lang/crates.io-index"2492
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"2493
+dependencies = [2494
+ "lock_api",2495
+ "parking_lot_core",2496
+]2497
+2498
+[[package]]2499
+name = "parking_lot_core"2500
+version = "0.9.12"2501
+source = "registry+https://github.com/rust-lang/crates.io-index"2502
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"2503
+dependencies = [2504
+ "cfg-if",2505
+ "libc",2506
+ "redox_syscall",2507
+ "smallvec",2508
+ "windows-link",2509
+]2510
+2511
+[[package]]2512
+name = "pbkdf2"2513
+version = "0.12.2"2514
+source = "registry+https://github.com/rust-lang/crates.io-index"2515
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"2516
+dependencies = [2517
+ "digest",2518
+]2519
+2520
+[[package]]2521
+name = "percent-encoding"2522
+version = "2.3.2"2523
+source = "registry+https://github.com/rust-lang/crates.io-index"2524
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"2525
+2526
+[[package]]2527
+name = "pest"2528
+version = "2.9.1"2529
+source = "registry+https://github.com/rust-lang/crates.io-index"2530
+checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad"2531
+dependencies = [2532
+ "memchr",2533
+ "ucd-trie",2534
+]2535
+2536
+[[package]]2537
+name = "pest_derive"2538
+version = "2.9.1"2539
+source = "registry+https://github.com/rust-lang/crates.io-index"2540
+checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f"2541
+dependencies = [2542
+ "pest",2543
+ "pest_generator",2544
+]2545
+2546
+[[package]]2547
+name = "pest_generator"2548
+version = "2.9.1"2549
+source = "registry+https://github.com/rust-lang/crates.io-index"2550
+checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5"2551
+dependencies = [2552
+ "pest",2553
+ "pest_meta",2554
+ "proc-macro2",2555
+ "quote",2556
+ "syn 2.0.119",2557
+]2558
+2559
+[[package]]2560
+name = "pest_meta"2561
+version = "2.9.1"2562
+source = "registry+https://github.com/rust-lang/crates.io-index"2563
+checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e"2564
+dependencies = [2565
+ "pest",2566
+]2567
+2568
+[[package]]2569
+name = "phf"2570
+version = "0.13.1"2571
+source = "registry+https://github.com/rust-lang/crates.io-index"2572
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"2573
+dependencies = [2574
+ "phf_shared",2575
+ "serde",2576
+]2577
+2578
+[[package]]2579
+name = "phf_codegen"2580
+version = "0.13.1"2581
+source = "registry+https://github.com/rust-lang/crates.io-index"2582
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"2583
+dependencies = [2584
+ "phf_generator",2585
+ "phf_shared",2586
+]2587
+2588
+[[package]]2589
+name = "phf_generator"2590
+version = "0.13.1"2591
+source = "registry+https://github.com/rust-lang/crates.io-index"2592
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"2593
+dependencies = [2594
+ "fastrand",2595
+ "phf_shared",2596
+]2597
+2598
+[[package]]2599
+name = "phf_shared"2600
+version = "0.13.1"2601
+source = "registry+https://github.com/rust-lang/crates.io-index"2602
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"2603
+dependencies = [2604
+ "siphasher",2605
+]2606
+2607
+[[package]]2608
+name = "pin-project-lite"2609
+version = "0.2.17"2610
+source = "registry+https://github.com/rust-lang/crates.io-index"2611
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"2612
+2613
+[[package]]2614
+name = "pkcs8"2615
+version = "0.10.2"2616
+source = "registry+https://github.com/rust-lang/crates.io-index"2617
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"2618
+dependencies = [2619
+ "der",2620
+ "spki",2621
+]2622
+2623
+[[package]]2624
+name = "pkg-config"2625
+version = "0.3.34"2626
+source = "registry+https://github.com/rust-lang/crates.io-index"2627
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"2628
+2629
+[[package]]2630
+name = "poly1305"2631
+version = "0.8.0"2632
+source = "registry+https://github.com/rust-lang/crates.io-index"2633
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"2634
+dependencies = [2635
+ "cpufeatures 0.2.17",2636
+ "opaque-debug",2637
+ "universal-hash",2638
+]2639
+2640
+[[package]]2641
+name = "potential_utf"2642
+version = "0.1.6"2643
+source = "registry+https://github.com/rust-lang/crates.io-index"2644
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"2645
+dependencies = [2646
+ "zerovec",2647
+]2648
+2649
+[[package]]2650
+name = "powerfmt"2651
+version = "0.2.0"2652
+source = "registry+https://github.com/rust-lang/crates.io-index"2653
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"2654
+2655
+[[package]]2656
+name = "ppv-lite86"2657
+version = "0.2.21"2658
+source = "registry+https://github.com/rust-lang/crates.io-index"2659
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"2660
+dependencies = [2661
+ "zerocopy",2662
+]2663
+2664
+[[package]]2665
+name = "precomputed-hash"2666
+version = "0.1.1"2667
+source = "registry+https://github.com/rust-lang/crates.io-index"2668
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"2669
+2670
+[[package]]2671
+name = "proc-macro-crate"2672
+version = "3.5.0"2673
+source = "registry+https://github.com/rust-lang/crates.io-index"2674
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"2675
+dependencies = [2676
+ "toml_edit",2677
+]2678
+2679
+[[package]]2680
+name = "proc-macro-error-attr2"2681
+version = "2.0.0"2682
+source = "registry+https://github.com/rust-lang/crates.io-index"2683
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"2684
+dependencies = [2685
+ "proc-macro2",2686
+ "quote",2687
+]2688
+2689
+[[package]]2690
+name = "proc-macro-error2"2691
+version = "2.0.1"2692
+source = "registry+https://github.com/rust-lang/crates.io-index"2693
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"2694
+dependencies = [2695
+ "proc-macro-error-attr2",2696
+ "proc-macro2",2697
+ "quote",2698
+]2699
+2700
+[[package]]2701
+name = "proc-macro2"2702
+version = "1.0.107"2703
+source = "registry+https://github.com/rust-lang/crates.io-index"2704
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"2705
+dependencies = [2706
+ "unicode-ident",2707
+]2708
+2709
+[[package]]2710
+name = "prost"2711
+version = "0.14.4"2712
+source = "registry+https://github.com/rust-lang/crates.io-index"2713
+checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"2714
+dependencies = [2715
+ "bytes",2716
+ "prost-derive",2717
+]2718
+2719
+[[package]]2720
+name = "prost-derive"2721
+version = "0.14.4"2722
+source = "registry+https://github.com/rust-lang/crates.io-index"2723
+checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"2724
+dependencies = [2725
+ "anyhow",2726
+ "itertools 0.14.0",2727
+ "proc-macro2",2728
+ "quote",2729
+ "syn 2.0.119",2730
+]2731
+2732
+[[package]]2733
+name = "quinn"2734
+version = "0.11.11"2735
+source = "registry+https://github.com/rust-lang/crates.io-index"2736
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"2737
+dependencies = [2738
+ "bytes",2739
+ "cfg_aliases",2740
+ "pin-project-lite",2741
+ "quinn-proto",2742
+ "quinn-udp",2743
+ "rustc-hash",2744
+ "rustls",2745
+ "socket2",2746
+ "thiserror 2.0.20",2747
+ "tokio",2748
+ "tracing",2749
+ "web-time",2750
+]2751
+2752
+[[package]]2753
+name = "quinn-proto"2754
+version = "0.11.17"2755
+source = "registry+https://github.com/rust-lang/crates.io-index"2756
+checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"2757
+dependencies = [2758
+ "aws-lc-rs",2759
+ "bytes",2760
+ "getrandom 0.4.3",2761
+ "lru-slab",2762
+ "rand 0.10.2",2763
+ "rand_pcg",2764
+ "ring",2765
+ "rustc-hash",2766
+ "rustls",2767
+ "rustls-pki-types",2768
+ "slab",2769
+ "thiserror 2.0.20",2770
+ "tinyvec",2771
+ "tracing",2772
+ "web-time",2773
+]2774
+2775
+[[package]]2776
+name = "quinn-udp"2777
+version = "0.5.15"2778
+source = "registry+https://github.com/rust-lang/crates.io-index"2779
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"2780
+dependencies = [2781
+ "cfg_aliases",2782
+ "libc",2783
+ "once_cell",2784
+ "socket2",2785
+ "tracing",2786
+ "windows-sys 0.61.2",2787
+]2788
+2789
+[[package]]2790
+name = "quote"2791
+version = "1.0.47"2792
+source = "registry+https://github.com/rust-lang/crates.io-index"2793
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"2794
+dependencies = [2795
+ "proc-macro2",2796
+]2797
+2798
+[[package]]2799
+name = "r-efi"2800
+version = "5.3.0"2801
+source = "registry+https://github.com/rust-lang/crates.io-index"2802
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"2803
+2804
+[[package]]2805
+name = "r-efi"2806
+version = "6.0.0"2807
+source = "registry+https://github.com/rust-lang/crates.io-index"2808
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"2809
+2810
+[[package]]2811
+name = "rand"2812
+version = "0.8.8"2813
+source = "registry+https://github.com/rust-lang/crates.io-index"2814
+checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"2815
+dependencies = [2816
+ "libc",2817
+ "rand_chacha 0.3.1",2818
+ "rand_core 0.6.4",2819
+]2820
+2821
+[[package]]2822
+name = "rand"2823
+version = "0.9.5"2824
+source = "registry+https://github.com/rust-lang/crates.io-index"2825
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"2826
+dependencies = [2827
+ "rand_chacha 0.9.0",2828
+ "rand_core 0.9.5",2829
+]2830
+2831
+[[package]]2832
+name = "rand"2833
+version = "0.10.2"2834
+source = "registry+https://github.com/rust-lang/crates.io-index"2835
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"2836
+dependencies = [2837
+ "chacha20 0.10.2",2838
+ "getrandom 0.4.3",2839
+ "rand_core 0.10.1",2840
+]2841
+2842
+[[package]]2843
+name = "rand_chacha"2844
+version = "0.3.1"2845
+source = "registry+https://github.com/rust-lang/crates.io-index"2846
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"2847
+dependencies = [2848
+ "ppv-lite86",2849
+ "rand_core 0.6.4",2850
+]2851
+2852
+[[package]]2853
+name = "rand_chacha"2854
+version = "0.9.0"2855
+source = "registry+https://github.com/rust-lang/crates.io-index"2856
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"2857
+dependencies = [2858
+ "ppv-lite86",2859
+ "rand_core 0.9.5",2860
+]2861
+2862
+[[package]]2863
+name = "rand_core"2864
+version = "0.6.4"2865
+source = "registry+https://github.com/rust-lang/crates.io-index"2866
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"2867
+dependencies = [2868
+ "getrandom 0.2.17",2869
+]2870
+2871
+[[package]]2872
+name = "rand_core"2873
+version = "0.9.5"2874
+source = "registry+https://github.com/rust-lang/crates.io-index"2875
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"2876
+dependencies = [2877
+ "getrandom 0.3.4",2878
+]2879
+2880
+[[package]]2881
+name = "rand_core"2882
+version = "0.10.1"2883
+source = "registry+https://github.com/rust-lang/crates.io-index"2884
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"2885
+2886
+[[package]]2887
+name = "rand_pcg"2888
+version = "0.10.2"2889
+source = "registry+https://github.com/rust-lang/crates.io-index"2890
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"2891
+dependencies = [2892
+ "rand_core 0.10.1",2893
+]2894
+2895
+[[package]]2896
+name = "rand_xoshiro"2897
+version = "0.7.0"2898
+source = "registry+https://github.com/rust-lang/crates.io-index"2899
+checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"2900
+dependencies = [2901
+ "rand_core 0.9.5",2902
+]2903
+2904
+[[package]]2905
+name = "readlock"2906
+version = "0.1.11"2907
+source = "registry+https://github.com/rust-lang/crates.io-index"2908
+checksum = "6da6f291b23556edd9edaf655a0be2ad8ef8002ff5f1bca62b264f3f58b53f34"2909
+2910
+[[package]]2911
+name = "readlock-tokio"2912
+version = "0.1.6"2913
+source = "registry+https://github.com/rust-lang/crates.io-index"2914
+checksum = "fc7e264f9ec4f3d112e8e2f214e8e7cb5cf3b83278f3570b7e00bfe13d3bd8ff"2915
+dependencies = [2916
+ "tokio",2917
+]2918
+2919
+[[package]]2920
+name = "redox_syscall"2921
+version = "0.5.18"2922
+source = "registry+https://github.com/rust-lang/crates.io-index"2923
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"2924
+dependencies = [2925
+ "bitflags",2926
+]2927
+2928
+[[package]]2929
+name = "regex"2930
+version = "1.13.1"2931
+source = "registry+https://github.com/rust-lang/crates.io-index"2932
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"2933
+dependencies = [2934
+ "aho-corasick",2935
+ "memchr",2936
+ "regex-automata",2937
+ "regex-syntax",2938
+]2939
+2940
+[[package]]2941
+name = "regex-automata"2942
+version = "0.4.18"2943
+source = "registry+https://github.com/rust-lang/crates.io-index"2944
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"2945
+dependencies = [2946
+ "aho-corasick",2947
+ "memchr",2948
+ "regex-syntax",2949
+]2950
+2951
+[[package]]2952
+name = "regex-syntax"2953
+version = "0.8.11"2954
+source = "registry+https://github.com/rust-lang/crates.io-index"2955
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"2956
+2957
+[[package]]2958
+name = "reqwest"2959
+version = "0.13.4"2960
+source = "registry+https://github.com/rust-lang/crates.io-index"2961
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"2962
+dependencies = [2963
+ "base64",2964
+ "bytes",2965
+ "futures-core",2966
+ "futures-util",2967
+ "h2",2968
+ "http",2969
+ "http-body",2970
+ "http-body-util",2971
+ "hyper",2972
+ "hyper-rustls",2973
+ "hyper-util",2974
+ "js-sys",2975
+ "log",2976
+ "percent-encoding",2977
+ "pin-project-lite",2978
+ "quinn",2979
+ "rustls",2980
+ "rustls-pki-types",2981
+ "rustls-platform-verifier",2982
+ "sync_wrapper",2983
+ "tokio",2984
+ "tokio-rustls",2985
+ "tokio-util",2986
+ "tower",2987
+ "tower-http",2988
+ "tower-service",2989
+ "url",2990
+ "wasm-bindgen",2991
+ "wasm-bindgen-futures",2992
+ "wasm-streams",2993
+ "web-sys",2994
+]2995
+2996
+[[package]]2997
+name = "ring"2998
+version = "0.17.14"2999
+source = "registry+https://github.com/rust-lang/crates.io-index"3000
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"3001
+dependencies = [3002
+ "cc",3003
+ "cfg-if",3004
+ "getrandom 0.2.17",3005
+ "libc",3006
+ "untrusted",3007
+ "windows-sys 0.52.0",3008
+]3009
+3010
+[[package]]3011
+name = "rmp"3012
+version = "0.8.15"3013
+source = "registry+https://github.com/rust-lang/crates.io-index"3014
+checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"3015
+dependencies = [3016
+ "num-traits",3017
+]3018
+3019
+[[package]]3020
+name = "rmp-serde"3021
+version = "1.3.1"3022
+source = "registry+https://github.com/rust-lang/crates.io-index"3023
+checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"3024
+dependencies = [3025
+ "rmp",3026
+ "serde",3027
+]3028
+3029
+[[package]]3030
+name = "ruma"3031
+version = "0.16.0"3032
+source = "registry+https://github.com/rust-lang/crates.io-index"3033
+checksum = "ee4fe5bfacdb0e95e733da3b6c37d98edf46447a4a8e8dea824e0da266d8ad59"3034
+dependencies = [3035
+ "assign",3036
+ "js_int",3037
+ "js_option",3038
+ "ruma-client-api",3039
+ "ruma-common",3040
+ "ruma-events",3041
+ "ruma-html",3042
+ "web-time",3043
+]3044
+3045
+[[package]]3046
+name = "ruma-client-api"3047
+version = "0.24.0"3048
+source = "registry+https://github.com/rust-lang/crates.io-index"3049
+checksum = "bf7ca43a888ca569168d7e3901f4dd14a777b860bb19f4c08e35414162eb261c"3050
+dependencies = [3051
+ "as_variant",3052
+ "assign",3053
+ "bytes",3054
+ "http",3055
+ "js_int",3056
+ "js_option",3057
+ "maplit",3058
+ "ruma-common",3059
+ "ruma-events",3060
+ "serde",3061
+ "serde_html_form",3062
+ "serde_json",3063
+ "thiserror 2.0.20",3064
+ "url",3065
+ "web-time",3066
+]3067
+3068
+[[package]]3069
+name = "ruma-common"3070
+version = "0.19.0"3071
+source = "registry+https://github.com/rust-lang/crates.io-index"3072
+checksum = "2c3b4f00112791b490acce57df1ce3eb3f88899b045bebcff8a29f75369640cc"3073
+dependencies = [3074
+ "as_variant",3075
+ "base64",3076
+ "bytes",3077
+ "date_header",3078
+ "form_urlencoded",3079
+ "getrandom 0.4.3",3080
+ "http",3081
+ "indexmap",3082
+ "js_int",3083
+ "konst",3084
+ "percent-encoding",3085
+ "rand 0.10.2",3086
+ "regex",3087
+ "ruma-identifiers-validation",3088
+ "ruma-macros",3089
+ "serde",3090
+ "serde_html_form",3091
+ "serde_json",3092
+ "thiserror 2.0.20",3093
+ "time",3094
+ "tracing",3095
+ "url",3096
+ "uuid",3097
+ "web-time",3098
+ "wildmatch",3099
+ "zeroize",3100
+]3101
+3102
+[[package]]3103
+name = "ruma-events"3104
+version = "0.34.0"3105
+source = "registry+https://github.com/rust-lang/crates.io-index"3106
+checksum = "85d2f90830fc131691349b96a69ff53444eb6c3e8dc7869c77961b43cfaf3344"3107
+dependencies = [3108
+ "as_variant",3109
+ "indexmap",3110
+ "js_int",3111
+ "js_option",3112
+ "ruma-common",3113
+ "ruma-html",3114
+ "ruma-macros",3115
+ "serde",3116
+ "serde_json",3117
+ "thiserror 2.0.20",3118
+ "tracing",3119
+ "web-time",3120
+ "wildmatch",3121
+ "zeroize",3122
+]3123
+3124
+[[package]]3125
+name = "ruma-html"3126
+version = "0.8.0"3127
+source = "registry+https://github.com/rust-lang/crates.io-index"3128
+checksum = "48d33a944650f4bbd2188dd204d39dd87a9a1498f14b3a13252910c90ed7cd43"3129
+dependencies = [3130
+ "as_variant",3131
+ "html5ever",3132
+ "tracing",3133
+ "wildmatch",3134
+]3135
+3136
+[[package]]3137
+name = "ruma-identifiers-validation"3138
+version = "0.12.1"3139
+source = "registry+https://github.com/rust-lang/crates.io-index"3140
+checksum = "9d6cff00317675f487c4e7ccfb18875a14c5a14867b51d13f2a826053f03c432"3141
+dependencies = [3142
+ "js_int",3143
+ "thiserror 2.0.20",3144
+]3145
+3146
+[[package]]3147
+name = "ruma-macros"3148
+version = "0.19.0"3149
+source = "registry+https://github.com/rust-lang/crates.io-index"3150
+checksum = "c8cfb39eaa9b9fd389126ff941e060b496add5cbbfef559d80c46e571dda459c"3151
+dependencies = [3152
+ "as_variant",3153
+ "cfg-if",3154
+ "proc-macro-crate",3155
+ "proc-macro2",3156
+ "quote",3157
+ "ruma-identifiers-validation",3158
+ "serde",3159
+ "syn 2.0.119",3160
+ "toml",3161
+]3162
+3163
+[[package]]3164
+name = "rusqlite"3165
+version = "0.37.0"3166
+source = "registry+https://github.com/rust-lang/crates.io-index"3167
+checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"3168
+dependencies = [3169
+ "bitflags",3170
+ "fallible-iterator",3171
+ "fallible-streaming-iterator",3172
+ "hashlink",3173
+ "libsqlite3-sys",3174
+ "smallvec",3175
+]3176
+3177
+[[package]]3178
+name = "rustc-hash"3179
+version = "2.1.3"3180
+source = "registry+https://github.com/rust-lang/crates.io-index"3181
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"3182
+3183
+[[package]]3184
+name = "rustc_version"3185
+version = "0.4.1"3186
+source = "registry+https://github.com/rust-lang/crates.io-index"3187
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"3188
+dependencies = [3189
+ "semver",3190
+]3191
+3192
+[[package]]3193
+name = "rustix"3194
+version = "1.1.4"3195
+source = "registry+https://github.com/rust-lang/crates.io-index"3196
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"3197
+dependencies = [3198
+ "bitflags",3199
+ "errno",3200
+ "libc",3201
+ "linux-raw-sys",3202
+ "windows-sys 0.61.2",3203
+]3204
+3205
+[[package]]3206
+name = "rustls"3207
+version = "0.23.44"3208
+source = "registry+https://github.com/rust-lang/crates.io-index"3209
+checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"3210
+dependencies = [3211
+ "aws-lc-rs",3212
+ "log",3213
+ "once_cell",3214
+ "rustls-pki-types",3215
+ "rustls-webpki",3216
+ "subtle",3217
+ "zeroize",3218
+]3219
+3220
+[[package]]3221
+name = "rustls-native-certs"3222
+version = "0.8.4"3223
+source = "registry+https://github.com/rust-lang/crates.io-index"3224
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"3225
+dependencies = [3226
+ "openssl-probe",3227
+ "rustls-pki-types",3228
+ "schannel",3229
+ "security-framework",3230
+]3231
+3232
+[[package]]3233
+name = "rustls-pki-types"3234
+version = "1.15.1"3235
+source = "registry+https://github.com/rust-lang/crates.io-index"3236
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"3237
+dependencies = [3238
+ "web-time",3239
+ "zeroize",3240
+]3241
+3242
+[[package]]3243
+name = "rustls-platform-verifier"3244
+version = "0.7.0"3245
+source = "registry+https://github.com/rust-lang/crates.io-index"3246
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"3247
+dependencies = [3248
+ "core-foundation",3249
+ "core-foundation-sys",3250
+ "jni",3251
+ "log",3252
+ "once_cell",3253
+ "rustls",3254
+ "rustls-native-certs",3255
+ "rustls-platform-verifier-android",3256
+ "rustls-webpki",3257
+ "security-framework",3258
+ "security-framework-sys",3259
+ "webpki-root-certs",3260
+ "windows-sys 0.61.2",3261
+]3262
+3263
+[[package]]3264
+name = "rustls-platform-verifier-android"3265
+version = "0.1.1"3266
+source = "registry+https://github.com/rust-lang/crates.io-index"3267
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"3268
+3269
+[[package]]3270
+name = "rustls-webpki"3271
+version = "0.103.15"3272
+source = "registry+https://github.com/rust-lang/crates.io-index"3273
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"3274
+dependencies = [3275
+ "aws-lc-rs",3276
+ "ring",3277
+ "rustls-pki-types",3278
+ "untrusted",3279
+]3280
+3281
+[[package]]3282
+name = "rustversion"3283
+version = "1.0.23"3284
+source = "registry+https://github.com/rust-lang/crates.io-index"3285
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"3286
+3287
+[[package]]3288
+name = "same-file"3289
+version = "1.0.6"3290
+source = "registry+https://github.com/rust-lang/crates.io-index"3291
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"3292
+dependencies = [3293
+ "winapi-util",3294
+]3295
+3296
+[[package]]3297
+name = "schannel"3298
+version = "0.1.29"3299
+source = "registry+https://github.com/rust-lang/crates.io-index"3300
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"3301
+dependencies = [3302
+ "windows-sys 0.61.2",3303
+]3304
+3305
+[[package]]3306
+name = "scoped-tls"3307
+version = "1.0.1"3308
+source = "registry+https://github.com/rust-lang/crates.io-index"3309
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"3310
+3311
+[[package]]3312
+name = "scopeguard"3313
+version = "1.2.0"3314
+source = "registry+https://github.com/rust-lang/crates.io-index"3315
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"3316
+3317
+[[package]]3318
+name = "sealed"3319
+version = "0.6.0"3320
+source = "registry+https://github.com/rust-lang/crates.io-index"3321
+checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"3322
+dependencies = [3323
+ "proc-macro2",3324
+ "quote",3325
+ "syn 2.0.119",3326
+]3327
+3328
+[[package]]3329
+name = "sealed"3330
+version = "0.7.0"3331
+source = "registry+https://github.com/rust-lang/crates.io-index"3332
+checksum = "9b68e2ea526d9fb32f23ca8894fb5da9e743f34c2f41701f0501dc8a25c4b343"3333
+dependencies = [3334
+ "proc-macro2",3335
+ "quote",3336
+ "syn 3.0.5",3337
+]3338
+3339
+[[package]]3340
+name = "security-framework"3341
+version = "3.7.0"3342
+source = "registry+https://github.com/rust-lang/crates.io-index"3343
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"3344
+dependencies = [3345
+ "bitflags",3346
+ "core-foundation",3347
+ "core-foundation-sys",3348
+ "libc",3349
+ "security-framework-sys",3350
+]3351
+3352
+[[package]]3353
+name = "security-framework-sys"3354
+version = "2.17.0"3355
+source = "registry+https://github.com/rust-lang/crates.io-index"3356
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"3357
+dependencies = [3358
+ "core-foundation-sys",3359
+ "libc",3360
+]3361
+3362
+[[package]]3363
+name = "semver"3364
+version = "1.0.28"3365
+source = "registry+https://github.com/rust-lang/crates.io-index"3366
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"3367
+3368
+[[package]]3369
+name = "serde"3370
+version = "1.0.229"3371
+source = "registry+https://github.com/rust-lang/crates.io-index"3372
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"3373
+dependencies = [3374
+ "serde_core",3375
+ "serde_derive",3376
+]3377
+3378
+[[package]]3379
+name = "serde-wasm-bindgen"3380
+version = "0.6.5"3381
+source = "registry+https://github.com/rust-lang/crates.io-index"3382
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"3383
+dependencies = [3384
+ "js-sys",3385
+ "serde",3386
+ "wasm-bindgen",3387
+]3388
+3389
+[[package]]3390
+name = "serde_bytes"3391
+version = "0.11.19"3392
+source = "registry+https://github.com/rust-lang/crates.io-index"3393
+checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"3394
+dependencies = [3395
+ "serde",3396
+ "serde_core",3397
+]3398
+3399
+[[package]]3400
+name = "serde_core"3401
+version = "1.0.229"3402
+source = "registry+https://github.com/rust-lang/crates.io-index"3403
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"3404
+dependencies = [3405
+ "serde_derive",3406
+]3407
+3408
+[[package]]3409
+name = "serde_derive"3410
+version = "1.0.229"3411
+source = "registry+https://github.com/rust-lang/crates.io-index"3412
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"3413
+dependencies = [3414
+ "proc-macro2",3415
+ "quote",3416
+ "syn 3.0.5",3417
+]3418
+3419
+[[package]]3420
+name = "serde_html_form"3421
+version = "0.4.1"3422
+source = "registry+https://github.com/rust-lang/crates.io-index"3423
+checksum = "8f0346d7a342ab90f405cfc08f25d15075f944f42fcabbc5eac923829fa6d228"3424
+dependencies = [3425
+ "form_urlencoded",3426
+ "indexmap",3427
+ "itoa",3428
+ "serde_core",3429
+ "zmij",3430
+]3431
+3432
+[[package]]3433
+name = "serde_json"3434
+version = "1.0.151"3435
+source = "registry+https://github.com/rust-lang/crates.io-index"3436
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"3437
+dependencies = [3438
+ "itoa",3439
+ "memchr",3440
+ "serde",3441
+ "serde_core",3442
+ "zmij",3443
+]3444
+3445
+[[package]]3446
+name = "serde_path_to_error"3447
+version = "0.1.20"3448
+source = "registry+https://github.com/rust-lang/crates.io-index"3449
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"3450
+dependencies = [3451
+ "itoa",3452
+ "serde",3453
+ "serde_core",3454
+]3455
+3456
+[[package]]3457
+name = "serde_spanned"3458
+version = "1.1.1"3459
+source = "registry+https://github.com/rust-lang/crates.io-index"3460
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"3461
+dependencies = [3462
+ "serde_core",3463
+]3464
+3465
+[[package]]3466
+name = "sha2"3467
+version = "0.10.9"3468
+source = "registry+https://github.com/rust-lang/crates.io-index"3469
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"3470
+dependencies = [3471
+ "cfg-if",3472
+ "cpufeatures 0.2.17",3473
+ "digest",3474
+]3475
+3476
+[[package]]3477
+name = "sharded-slab"3478
+version = "0.1.7"3479
+source = "registry+https://github.com/rust-lang/crates.io-index"3480
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"3481
+dependencies = [3482
+ "lazy_static",3483
+]3484
+3485
+[[package]]3486
+name = "shlex"3487
+version = "2.0.1"3488
+source = "registry+https://github.com/rust-lang/crates.io-index"3489
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"3490
+3491
+[[package]]3492
+name = "signal-hook-registry"3493
+version = "1.4.8"3494
+source = "registry+https://github.com/rust-lang/crates.io-index"3495
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"3496
+dependencies = [3497
+ "errno",3498
+ "libc",3499
+]3500
+3501
+[[package]]3502
+name = "signature"3503
+version = "2.2.0"3504
+source = "registry+https://github.com/rust-lang/crates.io-index"3505
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"3506
+dependencies = [3507
+ "rand_core 0.6.4",3508
+]3509
+3510
+[[package]]3511
+name = "simd-adler32"3512
+version = "0.3.10"3513
+source = "registry+https://github.com/rust-lang/crates.io-index"3514
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"3515
+3516
+[[package]]3517
+name = "simd_cesu8"3518
+version = "1.2.0"3519
+source = "registry+https://github.com/rust-lang/crates.io-index"3520
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"3521
+dependencies = [3522
+ "rustc_version",3523
+ "simdutf8",3524
+]3525
+3526
+[[package]]3527
+name = "simdutf8"3528
+version = "0.1.5"3529
+source = "registry+https://github.com/rust-lang/crates.io-index"3530
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"3531
+3532
+[[package]]3533
+name = "similar"3534
+version = "2.7.0"3535
+source = "registry+https://github.com/rust-lang/crates.io-index"3536
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"3537
+3538
+[[package]]3539
+name = "siphasher"3540
+version = "1.0.3"3541
+source = "registry+https://github.com/rust-lang/crates.io-index"3542
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"3543
+3544
+[[package]]3545
+name = "slab"3546
+version = "0.4.12"3547
+source = "registry+https://github.com/rust-lang/crates.io-index"3548
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"3549
+3550
+[[package]]3551
+name = "smallvec"3552
+version = "1.16.0"3553
+source = "registry+https://github.com/rust-lang/crates.io-index"3554
+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"3555
+dependencies = [3556
+ "serde",3557
+]3558
+3559
+[[package]]3560
+name = "socket2"3561
+version = "0.6.5"3562
+source = "registry+https://github.com/rust-lang/crates.io-index"3563
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"3564
+dependencies = [3565
+ "libc",3566
+ "windows-sys 0.61.2",3567
+]3568
+3569
+[[package]]3570
+name = "spki"3571
+version = "0.7.3"3572
+source = "registry+https://github.com/rust-lang/crates.io-index"3573
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"3574
+dependencies = [3575
+ "base64ct",3576
+ "der",3577
+]3578
+3579
+[[package]]3580
+name = "stable_deref_trait"3581
+version = "1.2.1"3582
+source = "registry+https://github.com/rust-lang/crates.io-index"3583
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"3584
+3585
+[[package]]3586
+name = "string_cache"3587
+version = "0.9.0"3588
+source = "registry+https://github.com/rust-lang/crates.io-index"3589
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"3590
+dependencies = [3591
+ "new_debug_unreachable",3592
+ "parking_lot",3593
+ "phf_shared",3594
+ "precomputed-hash",3595
+]3596
+3597
+[[package]]3598
+name = "string_cache_codegen"3599
+version = "0.6.1"3600
+source = "registry+https://github.com/rust-lang/crates.io-index"3601
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"3602
+dependencies = [3603
+ "phf_generator",3604
+ "phf_shared",3605
+ "proc-macro2",3606
+ "quote",3607
+]3608
+3609
+[[package]]3610
+name = "subtle"3611
+version = "2.6.1"3612
+source = "registry+https://github.com/rust-lang/crates.io-index"3613
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"3614
+3615
+[[package]]3616
+name = "syn"3617
+version = "1.0.109"3618
+source = "registry+https://github.com/rust-lang/crates.io-index"3619
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"3620
+dependencies = [3621
+ "proc-macro2",3622
+ "quote",3623
+ "unicode-ident",3624
+]3625
+3626
+[[package]]3627
+name = "syn"3628
+version = "2.0.119"3629
+source = "registry+https://github.com/rust-lang/crates.io-index"3630
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"3631
+dependencies = [3632
+ "proc-macro2",3633
+ "quote",3634
+ "unicode-ident",3635
+]3636
+3637
+[[package]]3638
+name = "syn"3639
+version = "3.0.5"3640
+source = "registry+https://github.com/rust-lang/crates.io-index"3641
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"3642
+dependencies = [3643
+ "proc-macro2",3644
+ "quote",3645
+ "unicode-ident",3646
+]3647
+3648
+[[package]]3649
+name = "sync_wrapper"3650
+version = "1.0.2"3651
+source = "registry+https://github.com/rust-lang/crates.io-index"3652
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"3653
+dependencies = [3654
+ "futures-core",3655
+]3656
+3657
+[[package]]3658
+name = "synstructure"3659
+version = "0.13.2"3660
+source = "registry+https://github.com/rust-lang/crates.io-index"3661
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"3662
+dependencies = [3663
+ "proc-macro2",3664
+ "quote",3665
+ "syn 2.0.119",3666
+]3667
+3668
+[[package]]3669
+name = "tempfile"3670
+version = "3.27.0"3671
+source = "registry+https://github.com/rust-lang/crates.io-index"3672
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"3673
+dependencies = [3674
+ "fastrand",3675
+ "getrandom 0.4.3",3676
+ "once_cell",3677
+ "rustix",3678
+ "windows-sys 0.61.2",3679
+]3680
+3681
+[[package]]3682
+name = "tendril"3683
+version = "0.5.1"3684
+source = "registry+https://github.com/rust-lang/crates.io-index"3685
+checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"3686
+dependencies = [3687
+ "new_debug_unreachable",3688
+]3689
+3690
+[[package]]3691
+name = "thiserror"3692
+version = "1.0.69"3693
+source = "registry+https://github.com/rust-lang/crates.io-index"3694
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"3695
+dependencies = [3696
+ "thiserror-impl 1.0.69",3697
+]3698
+3699
+[[package]]3700
+name = "thiserror"3701
+version = "2.0.20"3702
+source = "registry+https://github.com/rust-lang/crates.io-index"3703
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"3704
+dependencies = [3705
+ "thiserror-impl 2.0.20",3706
+]3707
+3708
+[[package]]3709
+name = "thiserror-impl"3710
+version = "1.0.69"3711
+source = "registry+https://github.com/rust-lang/crates.io-index"3712
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"3713
+dependencies = [3714
+ "proc-macro2",3715
+ "quote",3716
+ "syn 2.0.119",3717
+]3718
+3719
+[[package]]3720
+name = "thiserror-impl"3721
+version = "2.0.20"3722
+source = "registry+https://github.com/rust-lang/crates.io-index"3723
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"3724
+dependencies = [3725
+ "proc-macro2",3726
+ "quote",3727
+ "syn 3.0.5",3728
+]3729
+3730
+[[package]]3731
+name = "thread_local"3732
+version = "1.1.10"3733
+source = "registry+https://github.com/rust-lang/crates.io-index"3734
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"3735
+dependencies = [3736
+ "cfg-if",3737
+]3738
+3739
+[[package]]3740
+name = "time"3741
+version = "0.3.55"3742
+source = "registry+https://github.com/rust-lang/crates.io-index"3743
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"3744
+dependencies = [3745
+ "deranged",3746
+ "num-conv",3747
+ "powerfmt",3748
+ "serde_core",3749
+ "time-core",3750
+ "time-macros",3751
+]3752
+3753
+[[package]]3754
+name = "time-core"3755
+version = "0.1.9"3756
+source = "registry+https://github.com/rust-lang/crates.io-index"3757
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"3758
+3759
+[[package]]3760
+name = "time-macros"3761
+version = "0.2.32"3762
+source = "registry+https://github.com/rust-lang/crates.io-index"3763
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"3764
+dependencies = [3765
+ "num-conv",3766
+ "time-core",3767
+]3768
+3769
+[[package]]3770
+name = "tinystr"3771
+version = "0.8.4"3772
+source = "registry+https://github.com/rust-lang/crates.io-index"3773
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"3774
+dependencies = [3775
+ "displaydoc",3776
+ "zerovec",3777
+]3778
+3779
+[[package]]3780
+name = "tinyvec"3781
+version = "1.13.2"3782
+source = "registry+https://github.com/rust-lang/crates.io-index"3783
+checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"3784
+dependencies = [3785
+ "tinyvec_macros",3786
+]3787
+3788
+[[package]]3789
+name = "tinyvec_macros"3790
+version = "0.1.1"3791
+source = "registry+https://github.com/rust-lang/crates.io-index"3792
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"3793
+3794
+[[package]]3795
+name = "tokio"3796
+version = "1.53.1"3797
+source = "registry+https://github.com/rust-lang/crates.io-index"3798
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"3799
+dependencies = [3800
+ "bytes",3801
+ "libc",3802
+ "mio",3803
+ "pin-project-lite",3804
+ "signal-hook-registry",3805
+ "socket2",3806
+ "tokio-macros",3807
+ "windows-sys 0.61.2",3808
+]3809
+3810
+[[package]]3811
+name = "tokio-macros"3812
+version = "2.7.2"3813
+source = "registry+https://github.com/rust-lang/crates.io-index"3814
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"3815
+dependencies = [3816
+ "proc-macro2",3817
+ "quote",3818
+ "syn 3.0.5",3819
+]3820
+3821
+[[package]]3822
+name = "tokio-rustls"3823
+version = "0.26.5"3824
+source = "registry+https://github.com/rust-lang/crates.io-index"3825
+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"3826
+dependencies = [3827
+ "rustls",3828
+ "tokio",3829
+]3830
+3831
+[[package]]3832
+name = "tokio-stream"3833
+version = "0.1.19"3834
+source = "registry+https://github.com/rust-lang/crates.io-index"3835
+checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"3836
+dependencies = [3837
+ "futures-core",3838
+ "pin-project-lite",3839
+ "tokio",3840
+ "tokio-util",3841
+]3842
+3843
+[[package]]3844
+name = "tokio-util"3845
+version = "0.7.19"3846
+source = "registry+https://github.com/rust-lang/crates.io-index"3847
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"3848
+dependencies = [3849
+ "bytes",3850
+ "futures-core",3851
+ "futures-sink",3852
+ "libc",3853
+ "pin-project-lite",3854
+ "tokio",3855
+]3856
+3857
+[[package]]3858
+name = "toml"3859
+version = "1.1.5+spec-1.1.0"3860
+source = "registry+https://github.com/rust-lang/crates.io-index"3861
+checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785"3862
+dependencies = [3863
+ "serde_core",3864
+ "serde_spanned",3865
+ "toml_datetime",3866
+ "toml_parser",3867
+ "winnow",3868
+]3869
+3870
+[[package]]3871
+name = "toml_datetime"3872
+version = "1.1.1+spec-1.1.0"3873
+source = "registry+https://github.com/rust-lang/crates.io-index"3874
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"3875
+dependencies = [3876
+ "serde_core",3877
+]3878
+3879
+[[package]]3880
+name = "toml_edit"3881
+version = "0.25.13+spec-1.1.0"3882
+source = "registry+https://github.com/rust-lang/crates.io-index"3883
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"3884
+dependencies = [3885
+ "indexmap",3886
+ "toml_datetime",3887
+ "toml_parser",3888
+ "winnow",3889
+]3890
+3891
+[[package]]3892
+name = "toml_parser"3893
+version = "1.1.3+spec-1.1.0"3894
+source = "registry+https://github.com/rust-lang/crates.io-index"3895
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"3896
+dependencies = [3897
+ "winnow",3898
+]3899
+3900
+[[package]]3901
+name = "tower"3902
+version = "0.5.3"3903
+source = "registry+https://github.com/rust-lang/crates.io-index"3904
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"3905
+dependencies = [3906
+ "futures-core",3907
+ "futures-util",3908
+ "pin-project-lite",3909
+ "sync_wrapper",3910
+ "tokio",3911
+ "tower-layer",3912
+ "tower-service",3913
+]3914
+3915
+[[package]]3916
+name = "tower-http"3917
+version = "0.6.11"3918
+source = "registry+https://github.com/rust-lang/crates.io-index"3919
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"3920
+dependencies = [3921
+ "async-compression",3922
+ "bitflags",3923
+ "bytes",3924
+ "futures-core",3925
+ "futures-util",3926
+ "http",3927
+ "http-body",3928
+ "http-body-util",3929
+ "pin-project-lite",3930
+ "tokio",3931
+ "tokio-util",3932
+ "tower",3933
+ "tower-layer",3934
+ "tower-service",3935
+ "url",3936
+]3937
+3938
+[[package]]3939
+name = "tower-layer"3940
+version = "0.3.3"3941
+source = "registry+https://github.com/rust-lang/crates.io-index"3942
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"3943
+3944
+[[package]]3945
+name = "tower-service"3946
+version = "0.3.3"3947
+source = "registry+https://github.com/rust-lang/crates.io-index"3948
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"3949
+3950
+[[package]]3951
+name = "tracing"3952
+version = "0.1.44"3953
+source = "registry+https://github.com/rust-lang/crates.io-index"3954
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"3955
+dependencies = [3956
+ "pin-project-lite",3957
+ "tracing-attributes",3958
+ "tracing-core",3959
+]3960
+3961
+[[package]]3962
+name = "tracing-attributes"3963
+version = "0.1.31"3964
+source = "registry+https://github.com/rust-lang/crates.io-index"3965
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"3966
+dependencies = [3967
+ "proc-macro2",3968
+ "quote",3969
+ "syn 2.0.119",3970
+]3971
+3972
+[[package]]3973
+name = "tracing-core"3974
+version = "0.1.36"3975
+source = "registry+https://github.com/rust-lang/crates.io-index"3976
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"3977
+dependencies = [3978
+ "once_cell",3979
+ "valuable",3980
+]3981
+3982
+[[package]]3983
+name = "tracing-log"3984
+version = "0.2.0"3985
+source = "registry+https://github.com/rust-lang/crates.io-index"3986
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"3987
+dependencies = [3988
+ "log",3989
+ "once_cell",3990
+ "tracing-core",3991
+]3992
+3993
+[[package]]3994
+name = "tracing-subscriber"3995
+version = "0.3.23"3996
+source = "registry+https://github.com/rust-lang/crates.io-index"3997
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"3998
+dependencies = [3999
+ "matchers",4000
+ "nu-ansi-term",4001
+ "once_cell",4002
+ "regex-automata",4003
+ "sharded-slab",4004
+ "smallvec",4005
+ "thread_local",4006
+ "tracing",4007
+ "tracing-core",4008
+ "tracing-log",4009
+]4010
+4011
+[[package]]4012
+name = "try-lock"4013
+version = "0.2.5"4014
+source = "registry+https://github.com/rust-lang/crates.io-index"4015
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"4016
+4017
+[[package]]4018
+name = "typenum"4019
+version = "1.20.1"4020
+source = "registry+https://github.com/rust-lang/crates.io-index"4021
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"4022
+4023
+[[package]]4024
+name = "typewit"4025
+version = "1.15.2"4026
+source = "registry+https://github.com/rust-lang/crates.io-index"4027
+checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"4028
+4029
+[[package]]4030
+name = "ucd-trie"4031
+version = "0.1.7"4032
+source = "registry+https://github.com/rust-lang/crates.io-index"4033
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"4034
+4035
+[[package]]4036
+name = "ulid"4037
+version = "1.2.1"4038
+source = "registry+https://github.com/rust-lang/crates.io-index"4039
+checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"4040
+dependencies = [4041
+ "rand 0.9.5",4042
+ "web-time",4043
+]4044
+4045
+[[package]]4046
+name = "unicode-ident"4047
+version = "1.0.24"4048
+source = "registry+https://github.com/rust-lang/crates.io-index"4049
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"4050
+4051
+[[package]]4052
+name = "unicode-normalization"4053
+version = "0.1.25"4054
+source = "registry+https://github.com/rust-lang/crates.io-index"4055
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"4056
+dependencies = [4057
+ "tinyvec",4058
+]4059
+4060
+[[package]]4061
+name = "unicode-segmentation"4062
+version = "1.13.3"4063
+source = "registry+https://github.com/rust-lang/crates.io-index"4064
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"4065
+4066
+[[package]]4067
+name = "unicode-xid"4068
+version = "0.2.6"4069
+source = "registry+https://github.com/rust-lang/crates.io-index"4070
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"4071
+4072
+[[package]]4073
+name = "universal-hash"4074
+version = "0.5.1"4075
+source = "registry+https://github.com/rust-lang/crates.io-index"4076
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"4077
+dependencies = [4078
+ "crypto-common",4079
+ "subtle",4080
+]4081
+4082
+[[package]]4083
+name = "untrusted"4084
+version = "0.9.0"4085
+source = "registry+https://github.com/rust-lang/crates.io-index"4086
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"4087
+4088
+[[package]]4089
+name = "url"4090
+version = "2.5.8"4091
+source = "registry+https://github.com/rust-lang/crates.io-index"4092
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"4093
+dependencies = [4094
+ "form_urlencoded",4095
+ "idna",4096
+ "percent-encoding",4097
+ "serde",4098
+ "serde_derive",4099
+]4100
+4101
+[[package]]4102
+name = "urlencoding"4103
+version = "2.1.3"4104
+source = "registry+https://github.com/rust-lang/crates.io-index"4105
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"4106
+4107
+[[package]]4108
+name = "utf8_iter"4109
+version = "1.0.4"4110
+source = "registry+https://github.com/rust-lang/crates.io-index"4111
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"4112
+4113
+[[package]]4114
+name = "uuid"4115
+version = "1.26.0"4116
+source = "registry+https://github.com/rust-lang/crates.io-index"4117
+checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"4118
+dependencies = [4119
+ "getrandom 0.4.3",4120
+ "js-sys",4121
+ "serde_core",4122
+ "wasm-bindgen",4123
+]4124
+4125
+[[package]]4126
+name = "valuable"4127
+version = "0.1.1"4128
+source = "registry+https://github.com/rust-lang/crates.io-index"4129
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"4130
+4131
+[[package]]4132
+name = "vcpkg"4133
+version = "0.2.15"4134
+source = "registry+https://github.com/rust-lang/crates.io-index"4135
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"4136
+4137
+[[package]]4138
+name = "version_check"4139
+version = "0.9.5"4140
+source = "registry+https://github.com/rust-lang/crates.io-index"4141
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"4142
+4143
+[[package]]4144
+name = "vodozemac"4145
+version = "0.10.0"4146
+source = "registry+https://github.com/rust-lang/crates.io-index"4147
+checksum = "b98bf83c0992966775b8012f194b07b44928996163e5a05b741b43891571ae5b"4148
+dependencies = [4149
+ "aes",4150
+ "arrayvec",4151
+ "base64",4152
+ "base64ct",4153
+ "cbc",4154
+ "chacha20poly1305",4155
+ "curve25519-dalek",4156
+ "ed25519-dalek",4157
+ "getrandom 0.2.17",4158
+ "hkdf",4159
+ "hmac",4160
+ "matrix-pickle",4161
+ "prost",4162
+ "rand 0.8.8",4163
+ "serde",4164
+ "serde_bytes",4165
+ "serde_json",4166
+ "sha2",4167
+ "subtle",4168
+ "thiserror 2.0.20",4169
+ "x25519-dalek",4170
+ "zeroize",4171
+]4172
+4173
+[[package]]4174
+name = "walkdir"4175
+version = "2.5.0"4176
+source = "registry+https://github.com/rust-lang/crates.io-index"4177
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"4178
+dependencies = [4179
+ "same-file",4180
+ "winapi-util",4181
+]4182
+4183
+[[package]]4184
+name = "want"4185
+version = "0.3.1"4186
+source = "registry+https://github.com/rust-lang/crates.io-index"4187
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"4188
+dependencies = [4189
+ "try-lock",4190
+]4191
+4192
+[[package]]4193
+name = "wasi"4194
+version = "0.11.1+wasi-snapshot-preview1"4195
+source = "registry+https://github.com/rust-lang/crates.io-index"4196
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"4197
+4198
+[[package]]4199
+name = "wasip2"4200
+version = "1.0.4+wasi-0.2.12"4201
+source = "registry+https://github.com/rust-lang/crates.io-index"4202
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"4203
+dependencies = [4204
+ "wit-bindgen",4205
+]4206
+4207
+[[package]]4208
+name = "wasm-bindgen"4209
+version = "0.2.128"4210
+source = "registry+https://github.com/rust-lang/crates.io-index"4211
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"4212
+dependencies = [4213
+ "cfg-if",4214
+ "once_cell",4215
+ "rustversion",4216
+ "wasm-bindgen-macro",4217
+ "wasm-bindgen-shared",4218
+]4219
+4220
+[[package]]4221
+name = "wasm-bindgen-futures"4222
+version = "0.4.78"4223
+source = "registry+https://github.com/rust-lang/crates.io-index"4224
+checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"4225
+dependencies = [4226
+ "js-sys",4227
+ "wasm-bindgen",4228
+]4229
+4230
+[[package]]4231
+name = "wasm-bindgen-macro"4232
+version = "0.2.128"4233
+source = "registry+https://github.com/rust-lang/crates.io-index"4234
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"4235
+dependencies = [4236
+ "quote",4237
+ "wasm-bindgen-macro-support",4238
+]4239
+4240
+[[package]]4241
+name = "wasm-bindgen-macro-support"4242
+version = "0.2.128"4243
+source = "registry+https://github.com/rust-lang/crates.io-index"4244
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"4245
+dependencies = [4246
+ "bumpalo",4247
+ "proc-macro2",4248
+ "quote",4249
+ "syn 3.0.5",4250
+ "wasm-bindgen-shared",4251
+]4252
+4253
+[[package]]4254
+name = "wasm-bindgen-shared"4255
+version = "0.2.128"4256
+source = "registry+https://github.com/rust-lang/crates.io-index"4257
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"4258
+dependencies = [4259
+ "unicode-ident",4260
+]4261
+4262
+[[package]]4263
+name = "wasm-bindgen-test"4264
+version = "0.3.78"4265
+source = "registry+https://github.com/rust-lang/crates.io-index"4266
+checksum = "45863ef0bef521c12124eb39d9a38513c47db75f22e503c06beacd40afeb35db"4267
+dependencies = [4268
+ "async-trait",4269
+ "cast",4270
+ "js-sys",4271
+ "libm",4272
+ "minicov",4273
+ "nu-ansi-term",4274
+ "num-traits",4275
+ "oorandom",4276
+ "serde",4277
+ "serde_json",4278
+ "wasm-bindgen",4279
+ "wasm-bindgen-futures",4280
+ "wasm-bindgen-test-macro",4281
+ "wasm-bindgen-test-shared",4282
+]4283
+4284
+[[package]]4285
+name = "wasm-bindgen-test-macro"4286
+version = "0.3.78"4287
+source = "registry+https://github.com/rust-lang/crates.io-index"4288
+checksum = "8c89dcab8b516b6b603baca9d550b7282d68fcc7f367e3956cff7ebf406a3f12"4289
+dependencies = [4290
+ "proc-macro2",4291
+ "quote",4292
+ "syn 3.0.5",4293
+]4294
+4295
+[[package]]4296
+name = "wasm-bindgen-test-shared"4297
+version = "0.2.128"4298
+source = "registry+https://github.com/rust-lang/crates.io-index"4299
+checksum = "f37b4f992cebe528ef34964ae69681ac0fe7080071e7298e46008f9d380302af"4300
+4301
+[[package]]4302
+name = "wasm-streams"4303
+version = "0.5.0"4304
+source = "registry+https://github.com/rust-lang/crates.io-index"4305
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"4306
+dependencies = [4307
+ "futures-util",4308
+ "js-sys",4309
+ "wasm-bindgen",4310
+ "wasm-bindgen-futures",4311
+ "web-sys",4312
+]4313
+4314
+[[package]]4315
+name = "wasm_evt_listener"4316
+version = "0.1.0"4317
+source = "registry+https://github.com/rust-lang/crates.io-index"4318
+checksum = "dc92d6378b411ed94839112a36d9dbc77143451d85b05dfb0cce93a78dab1963"4319
+dependencies = [4320
+ "accessory",4321
+ "derivative",4322
+ "derive_more 1.0.0",4323
+ "fancy_constructor",4324
+ "futures-core",4325
+ "js-sys",4326
+ "smallvec",4327
+ "tokio",4328
+ "wasm-bindgen",4329
+ "web-sys",4330
+]4331
+4332
+[[package]]4333
+name = "web-sys"4334
+version = "0.3.105"4335
+source = "registry+https://github.com/rust-lang/crates.io-index"4336
+checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"4337
+dependencies = [4338
+ "js-sys",4339
+ "wasm-bindgen",4340
+]4341
+4342
+[[package]]4343
+name = "web-time"4344
+version = "1.1.0"4345
+source = "registry+https://github.com/rust-lang/crates.io-index"4346
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"4347
+dependencies = [4348
+ "js-sys",4349
+ "serde",4350
+ "wasm-bindgen",4351
+]4352
+4353
+[[package]]4354
+name = "web_atoms"4355
+version = "0.2.6"4356
+source = "registry+https://github.com/rust-lang/crates.io-index"4357
+checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"4358
+dependencies = [4359
+ "phf",4360
+ "phf_codegen",4361
+ "string_cache",4362
+ "string_cache_codegen",4363
+]4364
+4365
+[[package]]4366
+name = "webpki-root-certs"4367
+version = "1.0.9"4368
+source = "registry+https://github.com/rust-lang/crates.io-index"4369
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"4370
+dependencies = [4371
+ "rustls-pki-types",4372
+]4373
+4374
+[[package]]4375
+name = "webpki-roots"4376
+version = "1.0.9"4377
+source = "registry+https://github.com/rust-lang/crates.io-index"4378
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"4379
+dependencies = [4380
+ "rustls-pki-types",4381
+]4382
+4383
+[[package]]4384
+name = "wildmatch"4385
+version = "2.6.1"4386
+source = "registry+https://github.com/rust-lang/crates.io-index"4387
+checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68"4388
+4389
+[[package]]4390
+name = "winapi-util"4391
+version = "0.1.11"4392
+source = "registry+https://github.com/rust-lang/crates.io-index"4393
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"4394
+dependencies = [4395
+ "windows-sys 0.61.2",4396
+]4397
+4398
+[[package]]4399
+name = "windows-core"4400
+version = "0.62.2"4401
+source = "registry+https://github.com/rust-lang/crates.io-index"4402
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"4403
+dependencies = [4404
+ "windows-implement",4405
+ "windows-interface",4406
+ "windows-link",4407
+ "windows-result",4408
+ "windows-strings",4409
+]4410
+4411
+[[package]]4412
+name = "windows-implement"4413
+version = "0.60.2"4414
+source = "registry+https://github.com/rust-lang/crates.io-index"4415
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"4416
+dependencies = [4417
+ "proc-macro2",4418
+ "quote",4419
+ "syn 2.0.119",4420
+]4421
+4422
+[[package]]4423
+name = "windows-interface"4424
+version = "0.59.3"4425
+source = "registry+https://github.com/rust-lang/crates.io-index"4426
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"4427
+dependencies = [4428
+ "proc-macro2",4429
+ "quote",4430
+ "syn 2.0.119",4431
+]4432
+4433
+[[package]]4434
+name = "windows-link"4435
+version = "0.2.1"4436
+source = "registry+https://github.com/rust-lang/crates.io-index"4437
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"4438
+4439
+[[package]]4440
+name = "windows-result"4441
+version = "0.4.1"4442
+source = "registry+https://github.com/rust-lang/crates.io-index"4443
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"4444
+dependencies = [4445
+ "windows-link",4446
+]4447
+4448
+[[package]]4449
+name = "windows-strings"4450
+version = "0.5.1"4451
+source = "registry+https://github.com/rust-lang/crates.io-index"4452
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"4453
+dependencies = [4454
+ "windows-link",4455
+]4456
+4457
+[[package]]4458
+name = "windows-sys"4459
+version = "0.52.0"4460
+source = "registry+https://github.com/rust-lang/crates.io-index"4461
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"4462
+dependencies = [4463
+ "windows-targets",4464
+]4465
+4466
+[[package]]4467
+name = "windows-sys"4468
+version = "0.61.2"4469
+source = "registry+https://github.com/rust-lang/crates.io-index"4470
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"4471
+dependencies = [4472
+ "windows-link",4473
+]4474
+4475
+[[package]]4476
+name = "windows-targets"4477
+version = "0.52.6"4478
+source = "registry+https://github.com/rust-lang/crates.io-index"4479
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"4480
+dependencies = [4481
+ "windows_aarch64_gnullvm",4482
+ "windows_aarch64_msvc",4483
+ "windows_i686_gnu",4484
+ "windows_i686_gnullvm",4485
+ "windows_i686_msvc",4486
+ "windows_x86_64_gnu",4487
+ "windows_x86_64_gnullvm",4488
+ "windows_x86_64_msvc",4489
+]4490
+4491
+[[package]]4492
+name = "windows_aarch64_gnullvm"4493
+version = "0.52.6"4494
+source = "registry+https://github.com/rust-lang/crates.io-index"4495
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"4496
+4497
+[[package]]4498
+name = "windows_aarch64_msvc"4499
+version = "0.52.6"4500
+source = "registry+https://github.com/rust-lang/crates.io-index"4501
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"4502
+4503
+[[package]]4504
+name = "windows_i686_gnu"4505
+version = "0.52.6"4506
+source = "registry+https://github.com/rust-lang/crates.io-index"4507
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"4508
+4509
+[[package]]4510
+name = "windows_i686_gnullvm"4511
+version = "0.52.6"4512
+source = "registry+https://github.com/rust-lang/crates.io-index"4513
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"4514
+4515
+[[package]]4516
+name = "windows_i686_msvc"4517
+version = "0.52.6"4518
+source = "registry+https://github.com/rust-lang/crates.io-index"4519
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"4520
+4521
+[[package]]4522
+name = "windows_x86_64_gnu"4523
+version = "0.52.6"4524
+source = "registry+https://github.com/rust-lang/crates.io-index"4525
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"4526
+4527
+[[package]]4528
+name = "windows_x86_64_gnullvm"4529
+version = "0.52.6"4530
+source = "registry+https://github.com/rust-lang/crates.io-index"4531
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"4532
+4533
+[[package]]4534
+name = "windows_x86_64_msvc"4535
+version = "0.52.6"4536
+source = "registry+https://github.com/rust-lang/crates.io-index"4537
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"4538
+4539
+[[package]]4540
+name = "winnow"4541
+version = "1.0.4"4542
+source = "registry+https://github.com/rust-lang/crates.io-index"4543
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"4544
+dependencies = [4545
+ "memchr",4546
+]4547
+4548
+[[package]]4549
+name = "wiremock"4550
+version = "0.6.5"4551
+source = "registry+https://github.com/rust-lang/crates.io-index"4552
+checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"4553
+dependencies = [4554
+ "assert-json-diff",4555
+ "base64",4556
+ "deadpool 0.12.3",4557
+ "futures",4558
+ "http",4559
+ "http-body-util",4560
+ "hyper",4561
+ "hyper-util",4562
+ "log",4563
+ "once_cell",4564
+ "regex",4565
+ "serde",4566
+ "serde_json",4567
+ "tokio",4568
+ "url",4569
+]4570
+4571
+[[package]]4572
+name = "wit-bindgen"4573
+version = "0.57.1"4574
+source = "registry+https://github.com/rust-lang/crates.io-index"4575
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"4576
+4577
+[[package]]4578
+name = "writeable"4579
+version = "0.6.4"4580
+source = "registry+https://github.com/rust-lang/crates.io-index"4581
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"4582
+4583
+[[package]]4584
+name = "x25519-dalek"4585
+version = "2.0.1"4586
+source = "registry+https://github.com/rust-lang/crates.io-index"4587
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"4588
+dependencies = [4589
+ "curve25519-dalek",4590
+ "rand_core 0.6.4",4591
+ "serde",4592
+ "zeroize",4593
+]4594
+4595
+[[package]]4596
+name = "xxhash-rust"4597
+version = "0.8.18"4598
+source = "registry+https://github.com/rust-lang/crates.io-index"4599
+checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6"4600
+4601
+[[package]]4602
+name = "yoke"4603
+version = "0.8.3"4604
+source = "registry+https://github.com/rust-lang/crates.io-index"4605
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"4606
+dependencies = [4607
+ "stable_deref_trait",4608
+ "yoke-derive",4609
+ "zerofrom",4610
+]4611
+4612
+[[package]]4613
+name = "yoke-derive"4614
+version = "0.8.2"4615
+source = "registry+https://github.com/rust-lang/crates.io-index"4616
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"4617
+dependencies = [4618
+ "proc-macro2",4619
+ "quote",4620
+ "syn 2.0.119",4621
+ "synstructure",4622
+]4623
+4624
+[[package]]4625
+name = "zerocopy"4626
+version = "0.8.56"4627
+source = "registry+https://github.com/rust-lang/crates.io-index"4628
+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"4629
+dependencies = [4630
+ "zerocopy-derive",4631
+]4632
+4633
+[[package]]4634
+name = "zerocopy-derive"4635
+version = "0.8.56"4636
+source = "registry+https://github.com/rust-lang/crates.io-index"4637
+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"4638
+dependencies = [4639
+ "proc-macro2",4640
+ "quote",4641
+ "syn 2.0.119",4642
+]4643
+4644
+[[package]]4645
+name = "zerofrom"4646
+version = "0.1.8"4647
+source = "registry+https://github.com/rust-lang/crates.io-index"4648
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"4649
+dependencies = [4650
+ "zerofrom-derive",4651
+]4652
+4653
+[[package]]4654
+name = "zerofrom-derive"4655
+version = "0.1.7"4656
+source = "registry+https://github.com/rust-lang/crates.io-index"4657
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"4658
+dependencies = [4659
+ "proc-macro2",4660
+ "quote",4661
+ "syn 2.0.119",4662
+ "synstructure",4663
+]4664
+4665
+[[package]]4666
+name = "zeroize"4667
+version = "1.9.0"4668
+source = "registry+https://github.com/rust-lang/crates.io-index"4669
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"4670
+dependencies = [4671
+ "zeroize_derive",4672
+]4673
+4674
+[[package]]4675
+name = "zeroize_derive"4676
+version = "1.5.0"4677
+source = "registry+https://github.com/rust-lang/crates.io-index"4678
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"4679
+dependencies = [4680
+ "proc-macro2",4681
+ "quote",4682
+ "syn 2.0.119",4683
+]4684
+4685
+[[package]]4686
+name = "zerotrie"4687
+version = "0.2.5"4688
+source = "registry+https://github.com/rust-lang/crates.io-index"4689
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"4690
+dependencies = [4691
+ "displaydoc",4692
+ "yoke",4693
+ "zerofrom",4694
+]4695
+4696
+[[package]]4697
+name = "zerovec"4698
+version = "0.11.8"4699
+source = "registry+https://github.com/rust-lang/crates.io-index"4700
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"4701
+dependencies = [4702
+ "yoke",4703
+ "zerofrom",4704
+ "zerovec-derive",4705
+]4706
+4707
+[[package]]4708
+name = "zerovec-derive"4709
+version = "0.11.6"4710
+source = "registry+https://github.com/rust-lang/crates.io-index"4711
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"4712
+dependencies = [4713
+ "proc-macro2",4714
+ "quote",4715
+ "syn 3.0.5",4716
+]4717
+4718
+[[package]]4719
+name = "zlib-rs"4720
+version = "0.6.7"4721
+source = "registry+https://github.com/rust-lang/crates.io-index"4722
+checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"4723
+4724
+[[package]]4725
+name = "zmij"4726
+version = "1.0.23"4727
+source = "registry+https://github.com/rust-lang/crates.io-index"4728
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"@@ -0,0 +1,42 @@
1
+[package]2
+name = "macltok"3
+version = "0.1.0"4
+edition = "2024"5
+description = "A compact application backend on top of the Matrix Rust SDK"6
+7
+[features]8
+default = []9
+serde = ["dep:serde"]10
+sqlite = ["matrix-sdk/sqlite", "serde", "dep:url"]11
+bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]12
+13
+[dependencies]14
+futures-util = { version = "0.3.31", default-features = false }15
+http = "1"16
+matrix-sdk = { version = "0.18.0", default-features = false, features = ["e2e-encryption"] }17
+matrix-sdk-crypto = { version = "0.18.0", default-features = false }18
+matrix-sdk-ui = { version = "0.18.0", default-features = false }19
+ruma = { version = "0.16.0", default-features = false }20
+serde = { version = "1.0.219", features = ["derive", "rc"], optional = true }21
+thiserror = "2.0.16"22
+mime = "0.3.17"23
+tokio = { version = "1.47.1", features = ["rt", "sync"] }24
+tracing = "0.1.41"25
+url = { version = "2.5.7", optional = true }26
+27
+[dev-dependencies]28
+matrix-sdk = { version = "0.18.0", default-features = false, features = ["testing"] }29
+matrix-sdk-test = "0.18.0"30
+serde_json = "1.0.143"31
+tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "signal", "time"] }32
+wiremock = "0.6.5"33
+34
+[[example]]35
+name = "headless"36
+required-features = ["sqlite"]37
+38
+[lints.rust]39
+unsafe_code = "forbid"40
+41
+[lints.clippy]42
+all = "warn"@@ -0,0 +1,58 @@
1
+# macltok2
+3
+`macltok` is a compact Rust application backend built above `matrix-sdk` and4
+`matrix-sdk-ui`. It aims to provide the stateful behavior needed by a useful5
+Matrix client without depending on a GUI toolkit or inventing another protocol6
+SDK.7
+8
+The crate currently provides9
+10
+- typed commands, owned UI-safe events and bounded per-client transport11
+- Sliding Sync with normalized connection and room-list state12
+- independently retained live room timelines and backwards pagination13
+- text, emote, reply, edit, redaction, reaction, receipt and typing operations14
+- raw encrypted or unencrypted media acquisition with configurable byte limits15
+- encryption state, recovery-key import and SAS verification16
+- password login and session restoration with caller-owned encrypted SQLite stores17
+18
+Matrix room, event, user, device and transaction identifiers remain strongly19
+typed. A toolkit timeline identity covers local echoes without pretending they20
+already have remote event IDs.21
+22
+## Consuming the engine23
+24
+Run `Engine::run` and drain its single `EventStream` independently of command25
+submission. `ClientHandle::send` returns an enqueue ID; match that ID against26
+`CommandSucceeded`, `CommandSucceededWithRoom`, or `CommandFailed` before27
+reporting completion. A successful message command only enters the SDK send28
+queue; the timeline item's send state reports subsequent network delivery.29
+30
+Start a timeline from `TimelineReset`, then apply `TimelineChanged` diffs with31
+`TimelineDiff::try_apply`. If a diff does not match the current snapshot, stop32
+applying diffs for that timeline and send `TimelineCommand::Resync`. Resume only33
+after its fresh `TimelineReset`; the reset precedes the command completion.34
+35
+With `sqlite`, persist the initial `Session::record()` separately from its36
+encrypted SQLite store. On `SessionTokensChanged`, obtain a new record from37
+`ClientHandle::session_record()` and replace the saved record atomically in38
+protected storage. Restore with the same store path and passphrase. Delete the39
+saved credentials after `LoggedOut` or `SessionAccessRejected`, but keep them40
+after `Stopped` so a restart can restore the session. The headless example is41
+login-only; it does not persist sessions across runs.42
+43
+## Dependency policy44
+45
+UI frameworks, image decoders, application-directory discovery and logging46
+subscribers do not belong in this crate. SQLite remains optional through the47
+`sqlite` and `bundled-sqlite` features. Media decoding belongs to the consumer.48
+49
+## Verification50
+51
+```sh52
+nix develop --command cargo fmt -- --check53
+nix develop --command cargo test --all-targets54
+nix develop --command cargo test --all-targets --features sqlite55
+nix develop --command cargo clippy --all-targets -- -D warnings56
+nix develop --command cargo clippy --all-targets --features sqlite -- -D warnings57
+nix flake check58
+```@@ -0,0 +1,171 @@
1
+# macltok design decisions2
+3
+This document describes the contract implemented in the working tree.4
+5
+## Engine execution6
+7
+The engine keeps a contextual task registry. Every spawned task is registered8
+with either its command id and kind, a watcher name, or a room id and9
+generation. The run loop continuously joins completed tasks and removes their10
+context. A command-task panic produces its correlated `CommandFailed` event.11
+A watcher or current room-actor panic is terminal to the engine. Shutdown aborts12
+and joins the remaining registered tasks.13
+14
+The engine emits `Authenticated`, then `ConnectionChanged(Connecting)`, before15
+starting the sync service. `ConnectionChanged` also represents the later SDK16
+states and the final `Stopped` transition. `Engine::run` returns an17
+`EngineError` for terminal failure and emits `Failed` before `Stopped` when the18
+event stream remains available.19
+20
+The driver command channel remains lossless and clients must drain21
+`EventStream` independently of command submission. `ClientHandle` also exposes22
+a `watch` receiver for the current connection state. `RoomsChanged` is an23
+invalidation event. Call `ClientHandle::rooms` for current, normalised room24
+summaries.25
+26
+`ClientHandle` sends `Command` values and receives a `CommandId`. Completion is27
+reported as `CommandSucceeded`, `CommandSucceededWithRoom`, or28
+`CommandFailed`. `CommandFailure` contains the id, outer `CommandKind`, typed29
+`CommandErrorKind`, and display message. Direct request and response methods30
+return `CommandResultError`, which is either a delivered `CommandFailure` or a31
+closed reply channel.32
+33
+For timeline sends, success means the SDK send queue accepted the event;34
+network delivery is reported later through timeline send state.35
+36
+`send` reports enqueueing, not completion. Consumers retain the returned id37
+until a terminal result with that id arrives; they match `CommandFailure::kind`38
+rather than parsing its display message. The event stream must be drained while39
+commands are submitted, even when several frontend clients share one engine.40
+41
+## Room lifetime and ordering42
+43
+An open room has an explicit generation and exactly one of three states.44
+45
+- `Opening` owns the generation and actor request sender46
+- `Open` owns the generation and actor request sender47
+- `Closing` owns the generation and a bounded FIFO of deferred open and close commands48
+49
+An `Open` command starts an actor immediately and returns its terminal result50
+only after the actor reports readiness. There is no separate readiness waiter or51
+readiness task. Repeated opens while opening are retained by the actor and each52
+gets one terminal result. A close changes the room to `Closing`, rejects work53
+that needs an open actor, and waits for that generation to exit. Opens and54
+closes during closing are queued in order; after exit, each deferred command is55
+replayed, with a fresh generation started when needed. Events from earlier56
+generations are ignored.57
+58
+Each room actor serialises its timeline work in FIFO order. Different rooms run59
+concurrently. Room mutations which do not require an open timeline use a60
+separate keyed FIFO scheduler. Jobs with the same room, target, or direct-chat61
+key run in submission order while unrelated keys run concurrently. Notification62
+commands and encryption commands each have one dedicated serial worker. The63
+encryption worker owns the verification controller and cancels it before64
+shutdown completes.65
+66
+Pagination has a per-timeline mutex gate and a session-owned completion path.67
+Pagination tasks are reaped as they finish. Closing a timeline aborts its68
+pagination work and emits one terminal result for every still-pending command.69
+There is no public pagination handle. The internal result channel is only the70
+session completion mechanism and is not an engine-wide routing design.71
+72
+`TimelineDiff::try_apply` rejects indices that do not match the consumer's73
+snapshot without modifying that snapshot. A consumer then stops applying diffs74
+for that timeline and sends `TimelineCommand::Resync`. The timeline session75
+aborts and joins its old watcher, atomically subscribes to a fresh SDK snapshot76
+and diff stream, and emits `TimelineReset` before the resync command succeeds.77
+This works for rooms and threads without closing their timelines or pagination.78
+79
+Timeline diffs and resets carry `TimelineId`, which distinguishes a room from a80
+thread by its root event. Thread actors and pagination state belong to the room81
+actor. Stateful timeline commands require the room actor. Standalone room82
+commands such as typing, receipts, send-queue control, membership actions, and83
+moderation use the SDK `Room` directly.84
+85
+## Account construction and secrets86
+87
+`Session::login_password`, `Session::begin_oauth`, and `Session::restore` use88
+one shared account client builder. It configures encrypted SQLite storage,89
+encryption settings, refresh-token handling, and enabled threading without90
+thread subscriptions. `Session::build` passes the authenticated client to the91
+engine.92
+93
+Secret-bearing input types do not derive `Clone`. The store passphrase is94
+borrowed by the shared builder, password login passes its password by reference,95
+and authentication records are cloned only where SDK ownership requires it.96
+97
+The caller owns durable authentication records. Save `Session::record()` after98
+login, then consume `SessionTokensChanged` and atomically replace that saved99
+record with `ClientHandle::session_record()`. A previously captured record may100
+hold an exhausted refresh token; failing to persist a rotation must be surfaced101
+rather than silently treating the stale record as safe to restore. Restore with102
+the same `StoreConfig` path and passphrase. Explicit logout and103
+`SessionAccessRejected` invalidate the saved record and store; `Stopped` alone104
+does not. Neither cookies nor application-specific secret storage belong in the105
+toolkit.106
+107
+## Notifications and room data108
+109
+The notification client is constructed lazily in an `OnceCell` and shares the110
+engine sync service. It is created only when notification resolution is first111
+requested. `PushNotification` exposes typed room and sender metadata and a112
+typed `PushNotificationEvent`. The event is `Raw<AnySyncTimelineEvent>` for a113
+timeline notification or `Raw<StrippedRoomMemberEvent>` for an invite. SDK UI114
+types do not cross the public boundary.115
+116
+Room summaries are normalised once per `rooms` query. `ClientHandle::rooms_filtered`117
+normalises its name search term once before filtering. Summary generation runs118
+display-name requests with bounded concurrency. Sorting is descending latest119
+event timestamp, then case-insensitive name, then room id as a deterministic tie120
+break.121
+122
+Global and room account data retain typed Matrix event-type parameters and raw123
+event content. The handle offers direct raw reads and engine-backed raw writes.124
+125
+## Commands and errors126
+127
+`Command` is grouped into `Account`, `Room`, `Timeline`, `Media`,128
+`Encryption`, `Notification`, and `Lifecycle` domains. `CommandKind` identifies129
+the outer domain in result events.130
+131
+`RoomCommand::InviteUser` contains only room and user ids. It deliberately does132
+not expose a reason because the SDK invite helper accepts none. Kick, ban, and133
+unban retain optional reasons where the protocol supports them.134
+135
+`TimelineCommand::Edit` accepts `MessageEditContent`. Its `Text` and `Emote`136
+variants carry `RichText`, so edited text and emotes preserve plain text,137
+optional Matrix HTML, and mentions. Empty edited bodies fail with `EmptyBody`.138
+139
+`VerificationAction` contains `Confirm`, `Reject`, and `Cancel`. Starting a140
+verification is a target-bearing encryption command rather than an action.141
+Verification progress uses `Waiting`, `Comparison`, `Confirmed`, `Done`,142
+`Cancelled`, and typed `Failed(VerificationFailure)`. Request acceptance, SAS143
+startup and acceptance failures retain their message, while stream termination144
+has dedicated variants. A cancelled verification can be started again.145
+The watcher and action paths claim terminal emission so a racing cancellation146
+cannot replace `Done` with a local `Cancelled` event.147
+148
+`CommandErrorKind` is the stable typed failure category. It includes room and149
+timeline availability, queue pressure, validation, media, verification,150
+authentication, network, server, store, timeline, encryption, and fallback151
+categories. The rendered message preserves operation context while internal152
+errors retain their source chains.153
+154
+## Power levels155
+156
+Power-level reads return the complete local `RoomPowerLevels` mapping.157
+`PowerLevelChanges` applies selected common and well-known event thresholds,158
+and `SetUserPowerLevel` changes one user mapping.159
+160
+The SDK trait that applies a power-level event is crate-private. macltok keeps161
+an atomic local mapping, applies all requested changes to it, and sends one162
+state event. It does not patch upstream and does not send separate events for163
+individual fields.164
+165
+## Public model boundaries166
+167
+The public model uses owned ruma identifiers and typed data wherever it has a168
+stable representation. Raw Matrix event payloads remain `Raw` where arbitrary169
+event content must be preserved. Public output models are non-exhaustive where170
+they are expected to grow. SDK clients remain available through the handle for171
+direct state queries that do not need engine coordination.@@ -0,0 +1,393 @@
1
+# macltok review, 2026-09-242
+3
+Second full review, taken after the restructure and Tier 0 landed. It covers4
+correctness, races, robustness, duplication, antipatterns and how easy the crate5
+is to build a client on, then folds in the API gaps reported by Malice, the6
+first real client built on macltok.7
+8
+Every finding carries an id so fixes can reference it. Status starts as OPEN.9
+SDK claims cite matrix-sdk 0.18 source under `~/.cargo/registry` and were10
+checked against the code rather than recalled.11
+12
+Baseline when written. `cargo fmt --check` clean, all default-feature tests13
+pass. The sqlite and clippy runs are recorded at the end.14
+15
+## C. Correctness16
+17
+### C1. `Verify(Reject)` never rejects. OPEN18
+19
+`VerificationController::reject` (`src/encryption.rs:339-389`) validates the20
+SAS state twice and returns `Ok(())` without ever calling `sas.mismatch()`. The21
+peer is never told the emoji did not match, the local flow stays in22
+comparison, and the command reports success. A user who presses "they don't23
+match" gets no protection from the one action meant to provide it. No test24
+exercises `Reject`.25
+26
+### C2. Sync dies permanently after `SyncState::Error`. OPEN27
+28
+The connection watcher maps `SyncState::Error` to `ConnectionState::Offline`29
+(`src/engine.rs:1338-1341`) and nothing ever calls `SyncService::start` again.30
+In the SDK the supervisor breaks out of its loop on error, and even offline31
+mode ends in `Error` when its reachability check fails32
+(`matrix-sdk-ui sync_service.rs:313-339`). `start()` is the only way back33
+(`sync_service.rs:629-649`). The client sees "Offline" forever while the engine34
+still reports itself healthy. `SyncState::Terminated` has the same problem, it35
+becomes `ConnectionState::Stopped` while the engine keeps running, so36
+`Stopped` now means two different things.37
+38
+Fix direction. Restart with backoff on `Error` and `Terminated`, and report39
+`Stopped` only from engine shutdown.40
+41
+### C3. A room's send queue stays disabled after any send failure. OPEN42
+43
+The SDK disables a room's queue after every send error, recoverable or not44
+(`matrix-sdk send_queue/mod.rs:1011-1019`), and nothing in matrix-sdk-ui turns45
+it back on. macltok re-enables only inside `RetrySend`46
+(`src/engine.rs:2178`), and does so globally. One network blip therefore47
+pauses every later message in that room until the user explicitly retries a48
+specific transaction. Element X re-enables queues when sync returns to49
+running.50
+51
+Fix direction. Re-enable on each transition to `Online`, and keep52
+`RetrySend` for unwedging a single item.53
+54
+### C4. Own encrypted uploads cannot be fetched, and the media cache is split between two formats. OPEN55
+56
+macltok writes ciphertext into the media store under the `Encrypted` source key57
+and decrypts on every read (`src/engine.rs:2799-2821`, `2864-2874`). The SDK58
+stores plaintext under that same key, both in `Media::get_media_content`59
+(`matrix-sdk media.rs:451-480`, `522-528`) and in the send queue. After an60
+upload the send queue renames its plaintext cache entry to the remote61
+`Encrypted` source (`send_queue/upload.rs:928-953`). So fetching your own sent62
+image in an encrypted room reads plaintext, runs it through63
+`AttachmentDecryptor`, fails the hash check and returns `MediaUnavailable`. The64
+reverse also holds. Any SDK path that reads a macltok-written entry gets65
+ciphertext as if it were the image. The media tests force plaintext rooms66
+(`tests/media.rs:766`), which is why this passed.67
+68
+Fix direction. Cache plaintext, matching the SDK, and keep the byte bound by69
+decrypting in a streaming fashion before the write.70
+71
+### C5. Media single-flight is poisoned by a leader panic. OPEN72
+73
+In `spawn_media_fetch` (`src/engine.rs:1870-1911`) followers push their id and74
+return at once, trusting the leader to answer for them. If `fetch_media`75
+panics, the leader's own id gets "command task panicked", the followers get76
+nothing, and the `(MediaId, MediaSize)` entry is never removed. Every later77
+fetch of that media then becomes a follower of a dead leader and never78
+completes. Largely superseded if media fetch becomes a direct query (see79
+M2), but the fix is the same either way. Use a drop guard or a shared future80
+so the entry always clears.81
+82
+### C6. Back pagination can stick at "start reached" forever. OPEN83
+84
+`reached_start` is a cached `AtomicBool` that is only ever set from a85
+pagination result (`src/engine.rs:4060-4071`, `4097`). A limited sync with a86
+gap makes the event cache shrink to the last chunk87
+(`event_cache/caches/room/state.rs:917-924`), which the timeline applies as88
+`VectorDiff::Clear`. History is unloaded again but the flag stays true, so89
+every later `Paginate` emits `StartReached` without fetching anything. The90
+room scrolls to a stub and stops.91
+92
+Fix direction. Drop the flag and follow `Timeline::live_back_pagination_status`93
+(`matrix-sdk-ui timeline/pagination.rs:130-138`), which is authoritative.94
+95
+### C7. Soft logout destroys the store. OPEN96
+97
+`SessionChange::UnknownToken` carries `soft_logout`98
+(`matrix-sdk client/mod.rs:166`), but the watcher collapses it into a99
+payload-free `SessionAccessRejected` (`src/engine.rs:1314`). The README and the100
+event's doc comment both tell the client to delete the saved record and the101
+store. On a soft logout the correct response is to re-authenticate and keep102
+the store, so following the documentation throws away every E2EE key for103
+nothing.104
+105
+### C8. Logout and rejected access leave the engine running. OPEN106
+107
+`spawn_logout` emits `LoggedOut` and returns (`src/engine.rs:825-852`). The108
+sync service, watchers and room actors keep running against a dead session,109
+and every later command fails with an authentication error. Neither logout nor110
+a hard `UnknownToken` shuts the engine down. The engine should stop itself,111
+emitting `LoggedOut` or the rejection before `Stopped`.112
+113
+### C9. `MarkRead` can move the read marker backwards and costs two requests. OPEN114
+115
+`mark_read` (`src/engine.rs:2072-2095`) calls `Room::send_single_receipt`116
+twice. That call does no ordering check117
+(`matrix-sdk room/mod.rs:2167-2201`), so a client that marks an older event118
+drags both the public receipt and the fully-read marker back. The timeline API119
+refuses stale receipts (`timeline/controller/mod.rs:1621-1690`), and120
+`Room::send_multiple_receipts` sets fully read and read in one121
+`/read_markers` call (`room/mod.rs:2213-2230`). Private read receipts are not122
+offered at all.123
+124
+Fix direction. When the room is open, route through the actor and use125
+`Timeline::send_multiple_receipts`. Otherwise use a single126
+`Room::send_multiple_receipts`.127
+128
+### C10. `create_direct` always creates a new room. OPEN129
+130
+`Client::create_dm` never reuses an existing DM131
+(`matrix-sdk client/mod.rs:1830-1849`). Starting a chat with someone you132
+already talk to creates a duplicate every time. `Client::get_dm_room` exists133
+for the lookup (`client/mod.rs:1852`).134
+135
+### C11. Unregistered tasks swallow panics, contradicting the design doc. OPEN136
+137
+The design doc says a watcher panic is terminal. But the timeline watcher138
+(`src/engine.rs:589`), pagination worker (`446`), pagination result task139
+(`472`) and verification watcher (`src/encryption.rs:276`) are bare140
+`tokio::spawn`s. A panic in a timeline watcher freezes that timeline in141
+silence while commands keep succeeding. The reverse mistake also exists. The142
+account-data writers (`src/engine.rs:1111`, `1141`) and the per-room media143
+upload watcher (`1945`) are registered as `TaskContext::Watcher`, so a panic144
+in a single write kills the whole engine.145
+146
+### C12. The room actor blocks on network requests. OPEN147
+148
+Each room actor runs its commands serially and awaits each one. Most sends149
+only enqueue, but `Timeline::redact` on a remote event is a direct HTTP call150
+(`timeline/mod.rs:630`), and so is removing a reaction151
+(`controller/mod.rs:595`). `Room::make_reply_event` can fetch the replied-to152
+event (`room/reply.rs:108`). A slow homeserver therefore stalls every later153
+send, paginate and thread command in that room behind one redaction.154
+155
+### C13. Media registry evicts media that is still on screen. OPEN156
+157
+`MediaRegistry::insert_all` (`src/engine.rs:358-370`) evicts in first-insertion158
+order and does not refresh position on re-registration. One registry of 2048159
+entries is shared by every room. In a long-lived session, the first messages160
+loaded in a room that is still open are the first evicted, and fetching them161
+then fails with `MediaUnknown`. Closed timelines never release their entries,162
+so they compete with open ones.163
+164
+### C14. Smaller correctness issues. OPEN165
+166
+- `CommandResultError::Failed` renders its `Display` through `Debug`167
+ (`src/transport.rs:840`), so users see a struct dump.168
+- `download_bounded` builds a fresh `reqwest::Client` per fetch just to build a169
+ request (`src/engine.rs:3181`). That reloads the TLS roots every time.170
+ `http_client.get(...)` is enough. On a 401 it refreshes the token and then171
+ fails rather than retrying once.172
+- On `Lagged`, the session watcher assumes the dropped message was a token173
+ refresh (`src/engine.rs:1315`). A dropped `UnknownToken` is lost.174
+- Leaving a room does not close its open actor, so its timeline, typing175
+ subscription and room subscription outlive the membership.176
+- `open_room` on a room that is already open succeeds with no `TimelineReset`177
+ (`src/engine.rs:1608`). A second frontend sharing the engine gets success and178
+ no snapshot.179
+- On shutdown, in-flight commands never get a terminal result. That is180
+ acceptable, but the docs should say that `Stopped` fails everything still181
+ outstanding.182
+- A store path reused after a login whose record was never persisted holds a183
+ crypto account for another device, and the SDK refuses it. Document one184
+ store directory per login.185
+186
+## A. API shape and client-builder friendliness187
+188
+### A1. Three completion mechanisms, and the useful one is rationed. OPEN189
+190
+A client must know which of three patterns each operation uses.191
+192
+- `send` returns a `CommandId`. The client correlates it by hand against193
+ `CommandSucceeded`, `CommandSucceededWithRoom` or `CommandFailed` on a194
+ shared stream.195
+- Five direct methods (`join_room`, `create_room`, `create_direct`,196
+ `delete_devices`, and the internal room-result path) await a `oneshot` and197
+ return `CommandResultError`. These emit nothing on the stream.198
+- The account-data writes await a `oneshot` but return `ClientQueryError`,199
+ with no `CommandId` at all.200
+201
+The headless example is mostly bookkeeping of ids. Malice gap 4 comes straight202
+from this. The one pattern a web backend can use, an awaited result per203
+request, is not offered for most operations.204
+205
+### A2. Pure reads modelled as commands, against the doc's own rule. OPEN206
+207
+The design doc says operations are split by whether they change state. Media208
+fetch changes no state, yet it is a `MediaCommand` whose bytes arrive as a209
+`MediaReady` broadcast on the shared stream. Every frontend then receives every210
+other frontend's image bytes, up to 20 MiB each, and the requester has to211
+filter by `MediaId`.212
+213
+### A3. Every timeline item is a `Message`, with presentation strings in `body`. OPEN214
+215
+`normalise_timeline_item` (`src/normalise/timeline.rs`) flattens every event216
+into `TimelineMessage`. The consequences for a client are these.217
+218
+- Membership changes, profile changes and room state changes arrive as219
+ messages whose body reads `[m.room.member]` or similar.220
+- Redacted and undecryptable events carry hard-coded English bodies,221
+ `<message redacted>` and `<unable to decrypt message>`. Only UTD has a flag.222
+ A redaction cannot be told apart from a user who typed that text.223
+- `msgtype` is dropped, so emotes and notices cannot be rendered differently224
+ from text.225
+- No sender avatar, though the SDK profile has one. `display_name_ambiguous`226
+ is ignored, so two "Alice"s look identical.227
+- Reply references hold no preview content, and reply details are never228
+ fetched. The SDK leaves them `Unavailable` until the app calls229
+ `Timeline::fetch_details_for_event` (`timeline/event_item/content/reply.rs:38`),230
+ so replies to anything outside the loaded window never get a sender.231
+- `TimelineItem` is not `#[non_exhaustive]`, so adding a state-event variant232
+ later is a break.233
+234
+This needs a content enum, for example `Message { msgtype, ... }`, `Redacted`,235
+`UnableToDecrypt`, `Membership`, `ProfileChange`, `RoomState`, `Poll` and236
+`Other { event_type }`. It is the largest single rendering gap.237
+238
+### A4. Timeline commands cannot target local echoes. OPEN239
+240
+`Edit`, `Redact` and `ToggleReaction` take an `OwnedEventId` and wrap it in241
+`TimelineEventItemId::EventId`. The SDK accepts transaction ids for all three,242
+but a client cannot edit, redact or react to a message it sent until the243
+server echo arrives. The commands should take the identifier that the model244
+already exposes.245
+246
+### A5. `RoomSummary` is too thin for a room list. OPEN247
+248
+Missing fields, all cheap SDK reads. Topic, canonical alias, joined and invited249
+member counts, heroes, tombstone and successor, inviter for invites, a latest250
+event preview, and space parents and children (Malice gap 3).251
+252
+### A6. No incoming verification. OPEN253
+254
+Only outgoing `StartVerification` exists. A request started from another255
+device, which is the normal "verify this new login" flow, is never observed.256
+macltok registers no handler for it, and the SDK has no stream, so the app257
+pattern is an event handler plus `Encryption::get_verification_request`258
+(`encryption/mod.rs:1060`).259
+260
+### A7. Missing `#[non_exhaustive]`. OPEN261
+262
+Adding the attribute later is itself a breaking change, so it belongs now. Output types that will263
+grow are `TimelineItem`, `CommandKind`, `RoomMembership`, `VerificationState`,264
+`HistoryState`, `KeyBackupState` and `RecoveryState`. Input structs that will265
+grow need a constructor plus `#[non_exhaustive]`. These are266
+`CreateRoomOptions`, `PasswordLogin`, `OAuthLogin` and `UploadThumbnail`.267
+268
+### A8. Smaller API issues. OPEN269
+270
+- `MediaCommand::Upload` duplicates `UploadAttachment`, and the handle exposes271
+ both `upload_media` and `upload_attachment`.272
+- Convenience methods cover room and account commands but not timeline send,273
+ edit, paginate, mark read, typing, recovery or verification, so a client uses274
+ two styles side by side.275
+- `HistoryState::Failed(String)` and `RoomPowerLevels::events: Vec<(String, i64)>`276
+ are stringly typed where typed values exist.277
+- `rooms_filtered` re-implements the search half of `RoomListFilter::matches`,278
+ which itself lowercases the needle once per room.279
+- `RoomsChanged` is sent once per notable room-info update with no coalescing280
+ at source. A sync touching fifty rooms queues fifty invalidations, each of281
+ which prompts a full `rooms()` rebuild in a naive client.282
+- The encryption defaults (`auto_enable_backups`, `OneShot` backup download)283
+ are hard-coded with no override.284
+285
+## D. Duplication and antipatterns286
+287
+Line references are from the mechanical inventory taken during this review.288
+289
+- `RoomActor::run` is about 700 lines. The "queue while the thread is opening,290
+ else QueueFull" block appears four times (`src/engine.rs:3367-3402`,291
+ `3518-3549`, `3625-3644`), and emit-then-`break 'actor` about twenty times.292
+ A thread registry type with one `route(id, root, command)` method removes293
+ most of it.294
+- Six `spawn_*` helpers share one shape of clone, spawn, run, emit295
+ (`src/engine.rs:825-960`). `spawn_rename_device` hand-rolls the same thing296
+ without `emit_command_result`. `CommandFailure` is built by hand in nine297
+ places. The reply-or-event branch appears nine times. One `spawn_command(id,298
+ kind, future)` plus one completion sink covers all of them, and A1 removes299
+ the branch entirely.300
+- `media_http_request` (`src/engine.rs:2991-3175`) is six near-identical301
+ branches that differ by endpoint type and token mode.302
+- Twenty-one `client.get_room(...)` lookups with hand-written `RoomUnknown`303
+ errors, ten of them consecutive match arms in `execute_room_command`.304
+- `media_from_message` and `media_registration` walk `MessageType` twice for305
+ the same item. The descriptor can be derived from the registration.306
+- The verification controller repeats its "done, then cancelled, then error"307
+ terminal check eight times. It also carries an unused `_client` parameter,308
+ and the encryption worker builds a throwaway `VerificationController` to run309
+ `Recover`. `EncryptionWork` is a single-variant enum.310
+- `catch_unwind` with a "... panicked" string appears six times.311
+- Test helpers `receive_until`, `event_messages`, `diff_items`, `start_engine`312
+ and `stop` are copied across up to five test files. A shared313
+ `tests/support/mod.rs` removes them.314
+- `model.rs` has its test module in the middle of the file, with types315
+ defined after it.316
+317
+## T. Test coverage318
+319
+No test touches the whole encryption domain (recovery, backups, verification),320
+all notification commands, invite, kick, ban and unban, power levels,321
+`MarkRead`, `SetTyping`, `Redact`, `ToggleReaction`, `CloseThread`, `Logout`,322
+`RenameDevice`, `DeleteDevices`, `resolve_push_notification` or323
+`rooms_filtered`. No test uses an encrypted room, which is how C4 got through.324
+325
+## M. Malice gaps, synthesised326
+327
+Malice reported four gaps. Each one traces back to a finding above, and fixing328
+the underlying finding closes the gap without special-casing it.329
+330
+| Malice gap | Root cause | Proposed shape |331
+| --- | --- | --- |332
+| Own profile | Not exposed at all | `ClientHandle::own_profile()`, a direct read via `Account::fetch_user_profile` (`account.rs:353`), returning a `Profile { user_id, display_name, avatar }`. Add `user_profile(user_id)` alongside. Setting the name and avatar become commands. |333
+| MXC avatar download | Media is reachable only through registered `MediaId`s (A2) | `ClientHandle::mxc_media(uri, MediaRequest)` as a bounded direct query, sharing the download, bound and cache code with timeline media. |334
+| Space children | `RoomSummary` gap (A5) | Wrap `matrix_sdk_ui::spaces::SpaceService` (`spaces/mod.rs`), which offers top-level joined spaces, `joined_parents_of_child` and a paginated `/hierarchy` `SpaceRoomList`. |335
+| Timeline media bytes per request | A1 and A2 | `ClientHandle::media(media_id, size)` as an awaited direct query. Remove `MediaCommand::Fetch` and `Event::MediaReady`. |336
+337
+Malice's rule, that no `matrix-sdk` use remains in the app, holds once these338
+land. None of the four needs an escape hatch.339
+340
+## Discussion items341
+342
+These are genuine trade-offs rather than fixes, each with a recommendation.343
+344
+### Q1. How far to take awaited results345
+346
+Option 1. Keep `send` and add `handle.execute(command).await -> Result<CommandOutput, CommandFailure>`347
+by carrying an optional `oneshot` in `CommandEnvelope`. Stream result events348
+continue for `send`. This is additive, but it leaves two mechanisms and every349
+engine path still branches on reply versus event.350
+351
+Option 2. Make awaited results the only completion path. Remove `CommandId`,352
+`CommandSucceeded*`, `CommandFailed` and `InteractiveAuthRequired` from the353
+stream, which then carries only state changes. Fire-and-forget callers spawn a354
+task. Every engine path produces one `Result` and the nine reply-or-event355
+branches disappear.356
+357
+The cost of option 2 is ordering. Today a `TimelineReset` is guaranteed to358
+precede the `CommandSucceeded` for its `Open` or `Resync` on the same stream.359
+Across a stream and a `oneshot` the guarantee weakens to "enqueued before", so360
+a client draining the stream in another task can see the future resolve first.361
+Either `Open` and `Resync` return the snapshot in their result and stop362
+emitting `TimelineReset`, which then needs an epoch on `TimelineChanged` so363
+stale diffs from the old watcher can be discarded, or the contract states that364
+the snapshot arrives on the stream at some point after success. A second365
+frontend sharing the engine also stops seeing the other's outcomes, though it366
+still sees every resulting state change.367
+368
+Recommendation. Option 2 with snapshot-in-result and an epoch. It is the369
+larger break, but it is the shape every client ends up wrapping the current API370
+into, and breakage is free now.371
+372
+### Q2. Space children source373
+374
+`SpaceService` gives the full graph, including unjoined children, over the375
+network via `/hierarchy`. Local `m.space.child` state is instant but lists only376
+what sync delivered and cannot show rooms the user has not joined.377
+Recommendation. Own a `SpaceService` in the engine, as with `SyncService`. Put378
+`parent_spaces` and `child_rooms` on `RoomSummary` from local state for the379
+sidebar, and add a paginated hierarchy query for browsing.380
+381
+### Q3. Timeline content model depth382
+383
+A3 can be closed with a coarse enum (message, redacted, UTD, state, other) or384
+with typed variants for membership and profile changes, room name and topic,385
+polls and calls. Coarse is quick but pushes raw event parsing onto every386
+client. Recommendation. Typed variants for membership, profile, name, topic,387
+avatar and encryption changes, which cover what Element shows inline, plus388
+`Other { event_type }` as the escape.389
+390
+## Verification results391
+392
+- `cargo fmt -- --check` clean.393
+- `cargo test --all-targets` 49 passed, none failed.@@ -0,0 +1,92 @@
1
+use macltok::{2
+ Command, CommandKind, EngineConfig, Event, LifecycleCommand, PasswordLogin, RoomCommand,3
+ RoomMembership, Session, StoreConfig, TimelineCommand,4
+};5
+6
+#[tokio::main]7
+async fn main() -> Result<(), Box<dyn std::error::Error>> {8
+ let homeserver = required("MATRIX_HOMESERVER")?;9
+ let username = required("MATRIX_USERNAME")?;10
+ let password = required("MATRIX_PASSWORD")?;11
+ let store_path = required("MATRIX_STORE_PATH")?;12
+ let store_passphrase = required("MATRIX_STORE_PASSPHRASE")?;13
+ let session = Session::login_password(14
+ PasswordLogin {15
+ homeserver,16
+ username,17
+ password,18
+ device_display_name: Some("macltok headless example".into()),19
+ },20
+ StoreConfig::new(store_path, store_passphrase),21
+ )22
+ .await?;23
+ let (engine, handle, mut events) = session.build(EngineConfig::default()).await?;24
+ let running = tokio::spawn(engine.run());25
+ let mut room_open_id = None;26
+ let mut resync_id = None;27
+ let mut timeline_items = Vec::new();28
+29
+ loop {30
+ tokio::select! {31
+ event = events.recv() => {32
+ let Some(event) = event else { break; };33
+ match event {34
+ Event::RoomsChanged if room_open_id.is_none() => {35
+ if let Some(room) = handle36
+ .rooms()37
+ .await38
+ .into_iter()39
+ .find(|room| room.membership == RoomMembership::Joined)40
+ {41
+ room_open_id = Some(handle42
+ .send(Command::Room(RoomCommand::Open(room.room_id)))43
+ .await?);44
+ }45
+ }46
+ Event::TimelineReset { items, .. } => {47
+ timeline_items = items;48
+ resync_id = None;49
+ }50
+ Event::TimelineChanged { timeline_id, diffs } if resync_id.is_none() => {51
+ for diff in diffs {52
+ if diff.try_apply(&mut timeline_items).is_err() {53
+ resync_id = Some(handle.send(Command::Timeline(54
+ TimelineCommand::Resync { timeline_id },55
+ )).await?);56
+ break;57
+ }58
+ }59
+ if resync_id.is_none() {60
+ for item in &timeline_items {61
+ if let macltok::TimelineItem::Message(message) = item {62
+ println!("{} {}", message.sender_name, message.body);63
+ }64
+ }65
+ }66
+ }67
+ Event::CommandSucceeded { id, command: CommandKind::Room }68
+ if room_open_id == Some(id) => println!("room ready"),69
+ Event::CommandFailed(error) if room_open_id == Some(error.id)70
+ || resync_id == Some(error.id) => return Err(error.message.into()),71
+ Event::CommandFailed(error) => {72
+ eprintln!("{:?} {}", error.command, error.message);73
+ }74
+ Event::Stopped => break,75
+ _ => {}76
+ }77
+ }78
+ result = tokio::signal::ctrl_c() => {79
+ result?;80
+ handle81
+ .send(Command::Lifecycle(LifecycleCommand::Shutdown))82
+ .await?;83
+ }84
+ }85
+ }86
+ running.await??;87
+ Ok(())88
+}89
+90
+fn required(name: &str) -> Result<String, Box<dyn std::error::Error>> {91
+ std::env::var(name).map_err(|_| format!("{name} is required").into())92
+}@@ -0,0 +1,27 @@
1
+{2
+ "nodes": {3
+ "nixpkgs": {4
+ "locked": {5
+ "lastModified": 1788746152,6
+ "narHash": "sha256-RaKngusl5I8pNi8RfrVvsHq3NZe7eFWzDlb01+hEVqY=",7
+ "owner": "NixOS",8
+ "repo": "nixpkgs",9
+ "rev": "42f17a57f4f6e33b3de3dca0a2a5ea5233169d02",10
+ "type": "github"11
+ },12
+ "original": {13
+ "owner": "NixOS",14
+ "ref": "nixpkgs-unstable",15
+ "repo": "nixpkgs",16
+ "type": "github"17
+ }18
+ },19
+ "root": {20
+ "inputs": {21
+ "nixpkgs": "nixpkgs"22
+ }23
+ }24
+ },25
+ "root": "root",26
+ "version": 727
+}@@ -0,0 +1,62 @@
1
+{2
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";3
+4
+ outputs =5
+ { self, nixpkgs }:6
+ let7
+ forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;8
+ in9
+ {10
+ packages = forAllSystems (11
+ system:12
+ let13
+ pkgs = nixpkgs.legacyPackages.${system};14
+ in15
+ {16
+ default = pkgs.rustPlatform.buildRustPackage {17
+ pname = "macltok";18
+ version = "0.1.0";19
+ src = self;20
+ cargoLock.lockFile = ./Cargo.lock;21
+ preBuild = "export NIX_BUILD_CORES=2";22
+ SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";23
+ };24
+ }25
+ );26
+27
+ checks = forAllSystems (system: {28
+ default = self.packages.${system}.default;29
+ sqlite = nixpkgs.legacyPackages.${system}.rustPlatform.buildRustPackage {30
+ pname = "macltok-sqlite-check";31
+ version = "0.1.0";32
+ src = self;33
+ cargoLock.lockFile = ./Cargo.lock;34
+ preBuild = "export NIX_BUILD_CORES=2";35
+ buildFeatures = [ "sqlite" ];36
+ nativeBuildInputs = [ nixpkgs.legacyPackages.${system}.pkg-config ];37
+ buildInputs = [ nixpkgs.legacyPackages.${system}.sqlite ];38
+ SSL_CERT_FILE = "${nixpkgs.legacyPackages.${system}.cacert}/etc/ssl/certs/ca-bundle.crt";39
+ };40
+ });41
+42
+ devShells = forAllSystems (43
+ system:44
+ let45
+ pkgs = nixpkgs.legacyPackages.${system};46
+ in47
+ {48
+ default = pkgs.mkShell {49
+ packages = with pkgs; [50
+ cargo51
+ clippy52
+ pkg-config53
+ rust-analyzer54
+ rustc55
+ rustfmt56
+ sqlite57
+ ];58
+ };59
+ }60
+ );61
+ };62
+}@@ -0,0 +1,321 @@
1
+use std::{2
+ collections::HashMap,3
+ path::{Path, PathBuf},4
+};5
+6
+use matrix_sdk::{7
+ Client, ClientBuilder, ThreadingSupport,8
+ authentication::{9
+ AuthSession,10
+ matrix::MatrixSession,11
+ oauth::{12
+ ClientId, ClientRegistrationData, CsrfToken, OAuthSession, UserSession,13
+ registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},14
+ },15
+ },16
+ encryption::{BackupDownloadStrategy, EncryptionSettings},17
+ ruma::{OwnedDeviceId, serde::Raw},18
+};19
+use serde::{Deserialize, Serialize};20
+use thiserror::Error;21
+use url::Url;22
+23
+use crate::{ClientHandle, Engine, EngineConfig, EngineError, EventStream};24
+25
+pub struct Session {26
+ client: Client,27
+ record: SessionRecord,28
+}29
+30
+pub struct PasswordLogin {31
+ pub homeserver: String,32
+ pub username: String,33
+ pub password: String,34
+ pub device_display_name: Option<String>,35
+}36
+37
+pub struct OAuthLogin {38
+ pub homeserver: String,39
+ pub redirect_uri: String,40
+ pub client_uri: String,41
+ pub client_name: Option<String>,42
+ pub client_id: Option<String>,43
+ pub device_id: Option<OwnedDeviceId>,44
+ pub application_type: OAuthApplicationType,45
+}46
+47
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]48
+pub enum OAuthApplicationType {49
+ Native,50
+ Web,51
+}52
+53
+pub struct PendingOAuthLogin {54
+ client: Client,55
+ authorization_url: String,56
+ state: Option<CsrfToken>,57
+}58
+59
+impl PendingOAuthLogin {60
+ #[must_use]61
+ pub fn authorization_url(&self) -> &str {62
+ &self.authorization_url63
+ }64
+65
+ #[must_use]66
+ pub fn registered_client_id(&self) -> String {67
+ self.client68
+ .oauth()69
+ .client_id()70
+ .expect("OAuth client was registered before authorization")71
+ .as_str()72
+ .to_owned()73
+ }74
+75
+ #[must_use]76
+ pub const fn is_pending(&self) -> bool {77
+ self.state.is_some()78
+ }79
+80
+ pub async fn finish(&mut self, callback_url: &str) -> Result<Session, AccountError> {81
+ let callback_url = Url::parse(callback_url)82
+ .map_err(|error| account_error("parsing the OAuth callback URL", error))?;83
+ let state = self84
+ .state85
+ .take()86
+ .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;87
+ let oauth = self.client.oauth();88
+ if let Err(error) = oauth.finish_login(callback_url.into()).await {89
+ oauth.abort_login(&state).await;90
+ return Err(account_error("OAuth login", error));91
+ }92
+ Session::from_client(self.client.clone())93
+ }94
+95
+ pub async fn abort(mut self) {96
+ if let Some(state) = self.state.take() {97
+ self.client.oauth().abort_login(&state).await;98
+ }99
+ }100
+}101
+102
+pub struct StoreConfig {103
+ path: PathBuf,104
+ passphrase: String,105
+}106
+107
+impl StoreConfig {108
+ #[must_use]109
+ pub fn new(path: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {110
+ Self {111
+ path: path.into(),112
+ passphrase: passphrase.into(),113
+ }114
+ }115
+116
+ #[must_use]117
+ pub fn path(&self) -> &Path {118
+ &self.path119
+ }120
+}121
+122
+/// Serializable authentication secrets. Store this separately from the123
+/// encrypted SQLite store in protected, preferably encrypted storage.124
+/// A saved record must be replaced when `Event::SessionTokensChanged` arrives.125
+#[derive(Clone, Deserialize, Serialize)]126
+pub struct SessionRecord {127
+ homeserver: String,128
+ authentication: AuthenticationRecord,129
+}130
+131
+#[derive(Clone, Deserialize, Serialize)]132
+#[serde(tag = "type", content = "session", rename_all = "snake_case")]133
+enum AuthenticationRecord {134
+ Matrix(MatrixSession),135
+ OAuth {136
+ client_id: ClientId,137
+ user: UserSession,138
+ },139
+}140
+141
+impl SessionRecord {142
+ #[must_use]143
+ pub fn homeserver(&self) -> &str {144
+ &self.homeserver145
+ }146
+}147
+148
+#[derive(Debug, Error)]149
+pub enum AccountError {150
+ #[error("{context} failed")]151
+ Operation {152
+ context: &'static str,153
+ #[source]154
+ source: Box<dyn std::error::Error + Send + Sync>,155
+ },156
+ #[error("the homeserver returned no authenticated session")]157
+ MissingSession,158
+ #[error("the SDK returned an unsupported authentication session")]159
+ UnsupportedAuthentication,160
+ #[error("the OAuth callback was already submitted")]161
+ OAuthCallbackAlreadySubmitted,162
+ #[error(transparent)]163
+ Engine(#[from] EngineError),164
+}165
+166
+impl Session {167
+ pub async fn login_password(168
+ login: PasswordLogin,169
+ store: StoreConfig,170
+ ) -> Result<Self, AccountError> {171
+ let client = matrix_client_builder(&store)172
+ .server_name_or_homeserver_url(login.homeserver.trim())173
+ .build()174
+ .await175
+ .map_err(|error| account_error("building the Matrix client", error))?;176
+ let mut request = client177
+ .matrix_auth()178
+ .login_username(login.username.trim(), &login.password)179
+ .request_refresh_token();180
+ if let Some(name) = login.device_display_name.as_deref() {181
+ request = request.initial_device_display_name(name);182
+ }183
+ request184
+ .send()185
+ .await186
+ .map_err(|error| account_error("password login", error))?;187
+ Self::from_client(client)188
+ }189
+190
+ pub async fn begin_oauth(191
+ login: OAuthLogin,192
+ store: StoreConfig,193
+ ) -> Result<PendingOAuthLogin, AccountError> {194
+ let redirect_uri = Url::parse(login.redirect_uri.trim())195
+ .map_err(|error| account_error("parsing the OAuth redirect URI", error))?;196
+ let client_uri = Url::parse(login.client_uri.trim())197
+ .map_err(|error| account_error("parsing the OAuth client URI", error))?;198
+ let client = matrix_client_builder(&store)199
+ .server_name_or_homeserver_url(login.homeserver.trim())200
+ .build()201
+ .await202
+ .map_err(|error| account_error("building the Matrix client", error))?;203
+ let application_type = match login.application_type {204
+ OAuthApplicationType::Native => ApplicationType::Native,205
+ OAuthApplicationType::Web => ApplicationType::Web,206
+ };207
+ let mut metadata = ClientMetadata::new(208
+ application_type,209
+ vec![OAuthGrantType::AuthorizationCode {210
+ redirect_uris: vec![redirect_uri.clone()],211
+ }],212
+ Localized::new(client_uri, []),213
+ );214
+ metadata.client_name = login.client_name.map(|name| Localized::new(name, []));215
+ let metadata = Raw::new(&metadata)216
+ .map_err(|error| account_error("serializing OAuth client metadata", error))?;217
+ let mut registration = ClientRegistrationData::new(metadata);218
+ if let Some(client_id) = login.client_id {219
+ registration.static_registrations = Some(HashMap::from([(220
+ client.homeserver(),221
+ ClientId::new(client_id),222
+ )]));223
+ }224
+ let authorization = client225
+ .oauth()226
+ .login(redirect_uri, login.device_id, Some(registration), None)227
+ .build()228
+ .await229
+ .map_err(|error| account_error("starting OAuth login", error))?;230
+ Ok(PendingOAuthLogin {231
+ client,232
+ authorization_url: authorization.url.to_string(),233
+ state: Some(authorization.state),234
+ })235
+ }236
+237
+ /// Restore a saved record with the same store location and passphrase.238
+ /// The caller must also handle refresh, rejection, and explicit logout.239
+ pub async fn restore(record: SessionRecord, store: StoreConfig) -> Result<Self, AccountError> {240
+ let client = matrix_client_builder(&store)241
+ .homeserver_url(&record.homeserver)242
+ .build()243
+ .await244
+ .map_err(|error| account_error("reopening the Matrix store", error))?;245
+ let authentication = match record.authentication.clone() {246
+ AuthenticationRecord::Matrix(session) => AuthSession::Matrix(session),247
+ AuthenticationRecord::OAuth { client_id, user } => {248
+ OAuthSession { client_id, user }.into()249
+ }250
+ };251
+ client252
+ .restore_session(authentication)253
+ .await254
+ .map_err(|error| account_error("restoring the authenticated session", error))?;255
+ Ok(Self { client, record })256
+ }257
+258
+ /// Initial login credentials only. This snapshot does not track later259
+ /// token rotations; use `ClientHandle::session_record` after building.260
+ #[must_use]261
+ pub fn record(&self) -> SessionRecord {262
+ self.record.clone()263
+ }264
+265
+ pub async fn build(266
+ self,267
+ config: EngineConfig,268
+ ) -> Result<(Engine, ClientHandle, EventStream), AccountError> {269
+ Engine::from_client(self.client, config)270
+ .await271
+ .map_err(AccountError::from)272
+ }273
+274
+ fn from_client(client: Client) -> Result<Self, AccountError> {275
+ let record = session_record(&client)?;276
+ Ok(Self { client, record })277
+ }278
+}279
+280
+pub(crate) fn session_record(client: &Client) -> Result<SessionRecord, AccountError> {281
+ let authentication = match client.session().ok_or(AccountError::MissingSession)? {282
+ AuthSession::Matrix(session) => AuthenticationRecord::Matrix(session),283
+ AuthSession::OAuth(session) => {284
+ let OAuthSession { client_id, user } = *session;285
+ AuthenticationRecord::OAuth { client_id, user }286
+ }287
+ _ => return Err(AccountError::UnsupportedAuthentication),288
+ };289
+ Ok(SessionRecord {290
+ homeserver: client.homeserver().to_string(),291
+ authentication,292
+ })293
+}294
+295
+fn encryption_settings() -> EncryptionSettings {296
+ EncryptionSettings {297
+ auto_enable_cross_signing: true,298
+ backup_download_strategy: BackupDownloadStrategy::OneShot,299
+ auto_enable_backups: true,300
+ }301
+}302
+303
+fn matrix_client_builder(store: &StoreConfig) -> ClientBuilder {304
+ Client::builder()305
+ .with_encryption_settings(encryption_settings())306
+ .with_threading_support(ThreadingSupport::Enabled {307
+ with_subscriptions: false,308
+ })309
+ .handle_refresh_tokens()310
+ .sqlite_store(&store.path, Some(&store.passphrase))311
+}312
+313
+fn account_error(314
+ context: &'static str,315
+ source: impl std::error::Error + Send + Sync + 'static,316
+) -> AccountError {317
+ AccountError::Operation {318
+ context,319
+ source: Box::new(source),320
+ }321
+}@@ -0,0 +1,863 @@
1
+use std::sync::Arc;2
+3
+use futures_util::StreamExt;4
+use matrix_sdk::{5
+ Client,6
+ encryption::{7
+ VerificationState,8
+ backups::BackupState,9
+ recovery::RecoveryState as SdkRecoveryState,10
+ verification::{11
+ SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState,12
+ },13
+ },14
+ ruma::events::key::verification::VerificationMethod,15
+};16
+use ruma::{OwnedDeviceId, UserId};17
+use tokio::{sync::Mutex, task::JoinHandle};18
+19
+use crate::{20
+ CommandErrorKind, EncryptionState, Event, KeyBackupState, RecoveryState, VerificationAction,21
+ VerificationState as UiState, VerificationTarget, error::CommandError,22
+ model::VerificationFailure, transport::EventSender,23
+};24
+25
+pub(crate) fn encryption_state(26
+ device_id: OwnedDeviceId,27
+ recovery: SdkRecoveryState,28
+ verification: VerificationState,29
+ backup: BackupState,30
+) -> EncryptionState {31
+ EncryptionState {32
+ device_id,33
+ device_verified: matches!(verification, VerificationState::Verified),34
+ recovery: match recovery {35
+ SdkRecoveryState::Unknown => RecoveryState::Unknown,36
+ SdkRecoveryState::Disabled => RecoveryState::Disabled,37
+ SdkRecoveryState::Incomplete => RecoveryState::Incomplete,38
+ SdkRecoveryState::Enabled => RecoveryState::Enabled,39
+ },40
+ key_backup: match backup {41
+ BackupState::Unknown => KeyBackupState::Unknown,42
+ BackupState::Creating => KeyBackupState::Creating,43
+ BackupState::Enabling => KeyBackupState::Enabling,44
+ BackupState::Resuming => KeyBackupState::Resuming,45
+ BackupState::Enabled => KeyBackupState::Enabled,46
+ BackupState::Downloading => KeyBackupState::Downloading,47
+ BackupState::Disabling => KeyBackupState::Disabling,48
+ },49
+ }50
+}51
+52
+#[derive(Default)]53
+pub(crate) struct VerificationController {54
+ active: Arc<Mutex<VerificationSession>>,55
+ task: Option<JoinHandle<()>>,56
+}57
+58
+#[derive(Default)]59
+struct VerificationSession {60
+ request: Option<VerificationRequest>,61
+ sas: Option<SasVerification>,62
+}63
+64
+impl VerificationController {65
+ pub(crate) async fn apply(66
+ &mut self,67
+ _client: &Client,68
+ sender: EventSender,69
+ action: VerificationAction,70
+ ) -> Result<(), CommandError> {71
+ match action {72
+ VerificationAction::Confirm => {73
+ // Retain a handle across the operation: the watcher clears74
+ // `active` when it publishes a terminal event, but this clone75
+ // still observes the shared SDK state afterwards.76
+ let sas = self.active.lock().await.sas.clone();77
+ match self.confirm().await {78
+ Ok(()) => Ok(()),79
+ Err(error) if error.kind == CommandErrorKind::VerificationInactive => {80
+ Err(error)81
+ }82
+ // The confirm advanced local SDK state before its send failed,83
+ // so the retained session could report success on retry without84
+ // network traffic. Best-effort cancel (no lock held across the85
+ // network), clear for restart, then report the terminal state.86
+ Err(error) => {87
+ // Stop the watcher first so it cannot emit a stale88
+ // Confirmed from the local transition we are unwinding.89
+ self.stop_task().await;90
+ // The peer can complete or cancel the verification91
+ // while our own Done send fails. The stopped watcher may92
+ // already have published that terminal outcome (losing the93
+ // claim below); otherwise this path owns it and must emit94
+ // it instead of regressing to Cancelled.95
+ if sas.as_ref().is_some_and(|sas| sas.is_done()) {96
+ if take_verification(&self.active).await {97
+ let _ =98
+ sender.send(Event::VerificationChanged(UiState::Done)).await;99
+ }100
+ return Err(error);101
+ }102
+ if let Some(reason) = sas103
+ .as_ref()104
+ .and_then(|sas| sas.cancel_info())105
+ .map(|info| info.reason().to_owned())106
+ {107
+ if take_verification(&self.active).await {108
+ let _ = sender109
+ .send(Event::VerificationChanged(UiState::Cancelled(reason)))110
+ .await;111
+ }112
+ return Err(error);113
+ }114
+ if let Some(sas) = sas {115
+ if sas.cancel().await.is_err() {116
+ // The best-effort unwind already advanced117
+ // local state; clearing here keeps the failed118
+ // cancellation from looking retryable. The119
+ // send is owned only if the watcher has not120
+ // already claimed the session for its own121
+ // terminal outcome.122
+ if take_verification(&self.active).await {123
+ let _ = sender124
+ .send(Event::VerificationChanged(UiState::Cancelled(125
+ error.to_string(),126
+ )))127
+ .await;128
+ }129
+ return Err(error);130
+ }131
+ // The peer can complete while the unwind cancel is in132
+ // flight and the SDK call no-ops; the Done then owns the133
+ // terminal outcome, so report it instead of attributing134
+ // it to the send failure.135
+ if sas.is_done() {136
+ if take_verification(&self.active).await {137
+ let _ = sender138
+ .send(Event::VerificationChanged(UiState::Done))139
+ .await;140
+ }141
+ return Err(error);142
+ }143
+ if let Some(info) = sas.cancel_info() {144
+ // Our own unwind cancel just marked the flow; it does145
+ // not own the outcome yet, so fall through and report146
+ // the send failure as Cancelled below. Only a peer147
+ // cancellation racing the unwind owns it instead.148
+ if !info.cancelled_by_us() {149
+ if take_verification(&self.active).await {150
+ let _ = sender151
+ .send(Event::VerificationChanged(UiState::Cancelled(152
+ info.reason().to_owned(),153
+ )))154
+ .await;155
+ }156
+ return Err(CommandError::new(157
+ CommandErrorKind::VerificationInactive,158
+ info.reason().to_owned(),159
+ ));160
+ }161
+ }162
+ }163
+ if take_verification(&self.active).await {164
+ let _ = sender165
+ .send(Event::VerificationChanged(UiState::Cancelled(166
+ error.to_string(),167
+ )))168
+ .await;169
+ }170
+ Err(error)171
+ }172
+ }173
+ }174
+ VerificationAction::Reject => self.reject().await,175
+ VerificationAction::Cancel => {176
+ // The SDK no-ops a cancel of an already-settled flow, so a177
+ // failed claim inside `cancel` already reported the true178
+ // outcome as an error and owns no event here. Only the claim179
+ // winner emits the local Cancelled.180
+ let claimed = self.cancel_claimed(Some(&sender)).await?;181
+ if claimed {182
+ sender183
+ .send(Event::VerificationChanged(UiState::Cancelled(184
+ "cancelled by this client".to_owned(),185
+ )))186
+ .await187
+ .map_err(|source| {188
+ CommandError::with_source(189
+ CommandErrorKind::EventReceiverClosed,190
+ "event receiver has closed",191
+ source,192
+ )193
+ })?;194
+ }195
+ Ok(())196
+ }197
+ }198
+ }199
+200
+ pub(crate) async fn start(201
+ &mut self,202
+ client: &Client,203
+ sender: EventSender,204
+ target: VerificationTarget,205
+ ) -> Result<(), CommandError> {206
+ client207
+ .encryption()208
+ .wait_for_e2ee_initialization_tasks()209
+ .await;210
+ let own_user_id = client.user_id().ok_or_else(|| {211
+ CommandError::new(212
+ CommandErrorKind::Unauthenticated,213
+ "the Matrix client is not authenticated",214
+ )215
+ })?;216
+ if self.has_active_verification().await {217
+ self.cancel().await?;218
+ } else {219
+ self.stop_task().await;220
+ }221
+ let request = match &target {222
+ VerificationTarget::OwnIdentity => {223
+ request_identity_verification(client, own_user_id).await?224
+ }225
+ VerificationTarget::User(user_id) => {226
+ request_identity_verification(client, user_id).await?227
+ }228
+ VerificationTarget::Device { user_id, device_id } => {229
+ let device = client230
+ .encryption()231
+ .get_device(user_id, device_id)232
+ .await233
+ .map_err(|source| {234
+ CommandError::with_source(235
+ CommandErrorKind::Encryption,236
+ "reading the verification device failed",237
+ source,238
+ )239
+ })?240
+ .ok_or_else(|| {241
+ CommandError::new(242
+ CommandErrorKind::VerificationUnsupported,243
+ "the verification device is unavailable",244
+ )245
+ })?;246
+ device247
+ .request_verification_with_methods(vec![VerificationMethod::SasV1])248
+ .await249
+ .map_err(|source| {250
+ CommandError::from_sdk("starting device verification failed", source)251
+ })?252
+ }253
+ };254
+ self.active.lock().await.request = Some(request.clone());255
+ sender256
+ .send(Event::VerificationTargetChanged(target))257
+ .await258
+ .map_err(|source| {259
+ CommandError::with_source(260
+ CommandErrorKind::EventReceiverClosed,261
+ "event receiver has closed",262
+ source,263
+ )264
+ })?;265
+ sender266
+ .send(Event::VerificationChanged(UiState::Waiting))267
+ .await268
+ .map_err(|source| {269
+ CommandError::with_source(270
+ CommandErrorKind::EventReceiverClosed,271
+ "event receiver has closed",272
+ source,273
+ )274
+ })?;275
+ let active = self.active.clone();276
+ self.task = Some(tokio::spawn(async move {277
+ follow_request(request, active, sender).await;278
+ }));279
+ Ok(())280
+ }281
+282
+ async fn confirm(&self) -> Result<(), CommandError> {283
+ let sas = self.active.lock().await.sas.clone().ok_or_else(|| {284
+ CommandError::new(285
+ CommandErrorKind::VerificationInactive,286
+ "verification has not reached comparison",287
+ )288
+ })?;289
+ // Report a settled flow truthfully instead of as "not reached290
+ // comparison": the SDK would no-op the confirm with success.291
+ if sas.is_done() {292
+ return Err(CommandError::new(293
+ CommandErrorKind::VerificationInactive,294
+ "verification is already complete",295
+ ));296
+ }297
+ if let Some(info) = sas.cancel_info() {298
+ return Err(CommandError::new(299
+ CommandErrorKind::VerificationInactive,300
+ info.reason().to_owned(),301
+ ));302
+ }303
+ // Only the short-auth-string stage is confirmable. The SDK reports the304
+ // remote-MAC-arrived state through the same presentable flag, while an305
+ // early confirm would locally no-op and falsely report success.306
+ if !sas.can_be_presented() {307
+ return Err(CommandError::new(308
+ CommandErrorKind::VerificationInactive,309
+ "verification has not reached comparison",310
+ ));311
+ }312
+ // Our own confirm can move the flow to Done, so only Done that predates313
+ // the send means the SDK no-op'd on a stale session; a cancellation314
+ // racing the send must not report success.315
+ let was_done = sas.is_done();316
+ sas.confirm()317
+ .await318
+ .map_err(|source| CommandError::from_sdk("confirming verification failed", source))?;319
+ if sas.is_done() {320
+ if was_done {321
+ return Err(CommandError::new(322
+ CommandErrorKind::VerificationInactive,323
+ "verification is already complete",324
+ ));325
+ }326
+ } else if sas.is_cancelled() {327
+ let reason = sas328
+ .cancel_info()329
+ .map(|info| info.reason().to_owned())330
+ .unwrap_or_else(|| "verification was cancelled".to_owned());331
+ return Err(CommandError::new(332
+ CommandErrorKind::VerificationInactive,333
+ reason,334
+ ));335
+ }336
+ Ok(())337
+ }338
+339
+ async fn reject(&self) -> Result<(), CommandError> {340
+ let sas = self.active.lock().await.sas.clone().ok_or_else(|| {341
+ CommandError::new(342
+ CommandErrorKind::VerificationInactive,343
+ "verification has not reached comparison",344
+ )345
+ })?;346
+ // Report a settled flow truthfully instead of as "not reached347
+ // comparison": the SDK would no-op the mismatch with success.348
+ if sas.is_done() {349
+ return Err(CommandError::new(350
+ CommandErrorKind::VerificationInactive,351
+ "verification is already complete",352
+ ));353
+ }354
+ if let Some(info) = sas.cancel_info() {355
+ return Err(CommandError::new(356
+ CommandErrorKind::VerificationInactive,357
+ info.reason().to_owned(),358
+ ));359
+ }360
+ // Rejecting before the comparison stage would cancel a flow the user361
+ // never compared, so it requires the same presentable state.362
+ if !sas.can_be_presented() {363
+ return Err(CommandError::new(364
+ CommandErrorKind::VerificationInactive,365
+ "verification has not reached comparison",366
+ ));367
+ }368
+ // A peer completing the flow concurrently must not be reported as369
+ // rejected. A remote cancellation racing the send turns the SDK call370
+ // into a no-op success, so report it instead of a false success.371
+ if sas.is_done() {372
+ return Err(CommandError::new(373
+ CommandErrorKind::VerificationInactive,374
+ "verification is already complete",375
+ ));376
+ }377
+ if let Some(info) = sas.cancel_info() {378
+ // Our own mismatch just marked the flow cancelled-by-us; that is the379
+ // expected reject path and stays Ok. Only a peer cancel reports an380
+ // error.381
+ if !info.cancelled_by_us() {382
+ return Err(CommandError::new(383
+ CommandErrorKind::VerificationInactive,384
+ info.reason().to_owned(),385
+ ));386
+ }387
+ }388
+ Ok(())389
+ }390
+ pub(crate) async fn cancel(&mut self) -> Result<(), CommandError> {391
+ self.cancel_claimed(None).await.map(|_| ())392
+ }393
+394
+ /// Return whether this action won the claim for a local cancellation.395
+ /// The optional sender reports a peer terminal outcome when it races after396
+ /// the claim; starting a replacement verification does not publish it.397
+ async fn cancel_claimed(&mut self, sender: Option<&EventSender>) -> Result<bool, CommandError> {398
+ let (sas, request) = {399
+ let active = self.active.lock().await;400
+ (active.sas.clone(), active.request.clone())401
+ };402
+ if sas.is_none() && request.is_none() {403
+ return Err(CommandError::new(404
+ CommandErrorKind::VerificationInactive,405
+ "no verification is active",406
+ ));407
+ }408
+ // The watcher owns terminal emission and claims the session when it409
+ // publishes. If the SDK is already terminal, the watcher either already410
+ // reported it or is about to: leave session and watcher alone so the411
+ // outcome is emitted exactly once, and report it instead of letting the412
+ // SDK no-op success regress Done to "cancelled by this client".413
+ if let Some(sas) = sas.as_ref() {414
+ if sas.is_done() {415
+ return Err(CommandError::new(416
+ CommandErrorKind::VerificationInactive,417
+ "verification is already complete",418
+ ));419
+ }420
+ if let Some(info) = sas.cancel_info() {421
+ return Err(CommandError::new(422
+ CommandErrorKind::VerificationInactive,423
+ info.reason().to_owned(),424
+ ));425
+ }426
+ } else if let Some(request) = request.as_ref() {427
+ if request.is_done() {428
+ return Err(CommandError::new(429
+ CommandErrorKind::VerificationInactive,430
+ "verification is already complete",431
+ ));432
+ }433
+ if let Some(info) = request.cancel_info() {434
+ return Err(CommandError::new(435
+ CommandErrorKind::VerificationInactive,436
+ info.reason().to_owned(),437
+ ));438
+ }439
+ }440
+ // Own the terminal outcome from here: claim the session before441
+ // stopping the watcher so a watcher that already claimed (and will442
+ // emit its own terminal event) turns this into a truthful error443
+ // instead of a duplicate local cancel; no lock is held across sends.444
+ if !take_verification(&self.active).await {445
+ // Re-check the SDK: the claim loss may be a stale session after a446
+ // terminal outcome, or a concurrent start of a new flow. Only the447
+ // terminal case reports here; a live session belongs to the new448
+ // flow and must not be disturbed.449
+ if let Some(sas) = sas.as_ref() {450
+ if sas.is_done() {451
+ return Err(CommandError::new(452
+ CommandErrorKind::VerificationInactive,453
+ "verification is already complete",454
+ ));455
+ }456
+ if let Some(info) = sas.cancel_info() {457
+ return Err(CommandError::new(458
+ CommandErrorKind::VerificationInactive,459
+ info.reason().to_owned(),460
+ ));461
+ }462
+ } else if let Some(request) = request.as_ref() {463
+ if request.is_done() {464
+ return Err(CommandError::new(465
+ CommandErrorKind::VerificationInactive,466
+ "verification is already complete",467
+ ));468
+ }469
+ if let Some(info) = request.cancel_info() {470
+ return Err(CommandError::new(471
+ CommandErrorKind::VerificationInactive,472
+ info.reason().to_owned(),473
+ ));474
+ }475
+ }476
+ return Err(CommandError::new(477
+ CommandErrorKind::VerificationInactive,478
+ "no verification is active",479
+ ));480
+ }481
+ self.stop_task().await;482
+ if let Some(sas) = sas {483
+ let result = sas.cancel().await;484
+ if sas.is_done() {485
+ report_claimed_terminal(sender, UiState::Done).await;486
+ return Err(CommandError::new(487
+ CommandErrorKind::VerificationInactive,488
+ "verification is already complete",489
+ ));490
+ }491
+ if let Some(info) = sas.cancel_info()492
+ && !info.cancelled_by_us()493
+ {494
+ let reason = info.reason().to_owned();495
+ report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;496
+ return Err(CommandError::new(497
+ CommandErrorKind::VerificationInactive,498
+ reason,499
+ ));500
+ }501
+ if let Err(source) = result {502
+ let error = CommandError::from_sdk("cancelling verification failed", source);503
+ report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;504
+ return Err(error);505
+ }506
+ Ok(true)507
+ } else if let Some(request) = request {508
+ let result = request.cancel().await;509
+ if request.is_done() {510
+ report_claimed_terminal(sender, UiState::Done).await;511
+ return Err(CommandError::new(512
+ CommandErrorKind::VerificationInactive,513
+ "verification is already complete",514
+ ));515
+ }516
+ if let Some(info) = request.cancel_info()517
+ && !info.cancelled_by_us()518
+ {519
+ let reason = info.reason().to_owned();520
+ report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;521
+ return Err(CommandError::new(522
+ CommandErrorKind::VerificationInactive,523
+ reason,524
+ ));525
+ }526
+ if let Err(source) = result {527
+ let error = CommandError::from_sdk("cancelling verification failed", source);528
+ report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;529
+ return Err(error);530
+ }531
+ Ok(true)532
+ } else {533
+ Ok(false)534
+ }535
+ }536
+537
+ async fn has_active_verification(&self) -> bool {538
+ let active = self.active.lock().await;539
+ active.request.is_some() || active.sas.is_some()540
+ }541
+542
+ async fn stop_task(&mut self) {543
+ if let Some(task) = self.task.take() {544
+ task.abort();545
+ let _ = task.await;546
+ }547
+ }548
+549
+ pub(crate) async fn stop(&mut self) {550
+ // Shutdown path: terminate the watcher without touching the SDK so a551
+ // shutdown under lock cannot hang on network sends. No terminal event552
+ // is emitted; the session is simply forgotten locally.553
+ self.stop_task().await;554
+ clear_verification(&self.active).await;555
+ }556
+}557
+558
+async fn request_identity_verification(559
+ client: &Client,560
+ user_id: &UserId,561
+) -> Result<VerificationRequest, CommandError> {562
+ let identity = client563
+ .encryption()564
+ .request_user_identity(user_id)565
+ .await566
+ .map_err(|source| CommandError::from_sdk("requesting the user identity failed", source))?567
+ .ok_or_else(|| {568
+ CommandError::new(569
+ CommandErrorKind::NoCrossSigningIdentity,570
+ "the user has no cross-signing identity",571
+ )572
+ })?;573
+ identity574
+ .request_verification_with_methods(vec![VerificationMethod::SasV1])575
+ .await576
+ .map_err(|source| {577
+ CommandError::from_verification_request("starting verification failed", source)578
+ })579
+}580
+581
+async fn follow_request(582
+ request: VerificationRequest,583
+ active: Arc<Mutex<VerificationSession>>,584
+ sender: EventSender,585
+) {586
+ let mut changes = request.changes();587
+ while let Some(state) = changes.next().await {588
+ match state {589
+ VerificationRequestState::Created { .. } => {}590
+ VerificationRequestState::Requested { .. } => {591
+ if let Err(error) = request592
+ .accept_with_methods(vec![VerificationMethod::SasV1])593
+ .await594
+ {595
+ tracing::warn!(error = %error, "accepting verification request failed");596
+ // Claim the session so this terminal outcome is emitted exactly597
+ // once; a lost claim means an action path already owns it.598
+ report_watcher_terminal(599
+ &active,600
+ &sender,601
+ UiState::Failed(VerificationFailure::RequestAcceptance(error.to_string())),602
+ "request acceptance failure",603
+ )604
+ .await;605
+ return;606
+ }607
+ }608
+ VerificationRequestState::Ready { .. } => match request.start_sas().await {609
+ Ok(Some(sas)) => {610
+ follow_sas(sas, active.clone(), sender.clone()).await;611
+ return;612
+ }613
+ Ok(None) => {}614
+ Err(error) => {615
+ tracing::warn!(error = %error, "starting SAS verification failed");616
+ // Claim the session so this terminal outcome is emitted exactly617
+ // once; a lost claim means an action path already owns it.618
+ report_watcher_terminal(619
+ &active,620
+ &sender,621
+ UiState::Failed(VerificationFailure::SasStartup(error.to_string())),622
+ "SAS startup failure",623
+ )624
+ .await;625
+ return;626
+ }627
+ },628
+ VerificationRequestState::Transitioned { verification } => {629
+ if let Verification::SasV1(sas) = verification {630
+ follow_sas(sas, active.clone(), sender.clone()).await;631
+ } else {632
+ report_watcher_terminal(633
+ &active,634
+ &sender,635
+ UiState::Cancelled(636
+ "the other device selected an unsupported method".into(),637
+ ),638
+ "unsupported verification method",639
+ )640
+ .await;641
+ }642
+ return;643
+ }644
+ VerificationRequestState::Done => {645
+ report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;646
+ return;647
+ }648
+ VerificationRequestState::Cancelled(info) => {649
+ report_watcher_terminal(650
+ &active,651
+ &sender,652
+ UiState::Cancelled(info.reason().to_owned()),653
+ "cancellation",654
+ )655
+ .await;656
+ return;657
+ }658
+ }659
+ }660
+ tracing::debug!("verification request watcher ended unexpectedly");661
+ // The stream may end after the SDK already settled without delivering the662
+ // terminal update; report the settled outcome instead of StreamEnded, and663
+ // stay silent if an action path already claimed the session.664
+ if request.is_done() {665
+ report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;666
+ } else if let Some(info) = request.cancel_info() {667
+ let reason = info.reason().to_owned();668
+ report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;669
+ } else {670
+ report_watcher_terminal(671
+ &active,672
+ &sender,673
+ UiState::Failed(VerificationFailure::RequestStreamEnded),674
+ "request stream termination",675
+ )676
+ .await;677
+ }678
+}679
+680
+async fn follow_sas(681
+ sas: SasVerification,682
+ active: Arc<Mutex<VerificationSession>>,683
+ sender: EventSender,684
+) {685
+ active.lock().await.sas = Some(sas.clone());686
+ let mut changes = sas.changes();687
+ while let Some(state) = changes.next().await {688
+ let update = match state {689
+ SasState::Created { .. } | SasState::Accepted { .. } => continue,690
+ SasState::Started { .. } => {691
+ if let Err(error) = sas.accept().await {692
+ tracing::warn!(error = %error, "accepting SAS verification failed");693
+ // Claim the session so this terminal outcome is emitted exactly694
+ // once; a lost claim means an action path already owns it.695
+ report_watcher_terminal(696
+ &active,697
+ &sender,698
+ UiState::Failed(VerificationFailure::SasAcceptance(error.to_string())),699
+ "SAS acceptance failure",700
+ )701
+ .await;702
+ return;703
+ }704
+ continue;705
+ }706
+ SasState::KeysExchanged { emojis, decimals } => UiState::Comparison {707
+ emojis: emojis708
+ .map(|short_auth| {709
+ short_auth710
+ .emojis711
+ .into_iter()712
+ .map(|emoji| (emoji.symbol.to_owned(), emoji.description.to_owned()))713
+ .collect()714
+ })715
+ .unwrap_or_default(),716
+ decimals,717
+ },718
+ SasState::Confirmed => UiState::Confirmed,719
+ SasState::Done { .. } => UiState::Done,720
+ SasState::Cancelled(info) => UiState::Cancelled(info.reason().to_owned()),721
+ };722
+ let terminal = matches!(update, UiState::Done | UiState::Cancelled(_));723
+ // Claim the session before the terminal send so a racing Cancel observes724
+ // no active session and the SDK no-op cannot regress Done back to725
+ // "cancelled by this client". A lost claim means an action path already726
+ // owns the terminal outcome, so stay silent instead of duplicating it.727
+ if terminal && !take_verification(&active).await {728
+ return;729
+ }730
+ if sender731
+ .send(Event::VerificationChanged(update))732
+ .await733
+ .is_err()734
+ {735
+ tracing::debug!("SAS watcher stopped because the event receiver closed");736
+ return;737
+ }738
+ if terminal {739
+ return;740
+ }741
+ }742
+ tracing::debug!("SAS watcher ended unexpectedly");743
+ // The stream may end after the SDK already settled without delivering the744
+ // terminal update; report the settled outcome instead of StreamEnded, and745
+ // stay silent if an action path already claimed the session.746
+ if sas.is_done() {747
+ report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;748
+ } else if let Some(info) = sas.cancel_info() {749
+ let reason = info.reason().to_owned();750
+ report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;751
+ } else {752
+ report_watcher_terminal(753
+ &active,754
+ &sender,755
+ UiState::Failed(VerificationFailure::SasStreamEnded),756
+ "SAS stream termination",757
+ )758
+ .await;759
+ }760
+}761
+762
+async fn report_watcher_terminal(763
+ active: &Arc<Mutex<VerificationSession>>,764
+ sender: &EventSender,765
+ state: UiState,766
+ context: &'static str,767
+) {768
+ if take_verification(active).await769
+ && sender770
+ .send(Event::VerificationChanged(state))771
+ .await772
+ .is_err()773
+ {774
+ tracing::debug!(775
+ context,776
+ "verification watcher could not report terminal outcome"777
+ );778
+ }779
+}780
+781
+async fn report_claimed_terminal(sender: Option<&EventSender>, state: UiState) {782
+ if let Some(sender) = sender {783
+ let _ = sender.send(Event::VerificationChanged(state)).await;784
+ }785
+}786
+787
+async fn clear_verification(active: &Arc<Mutex<VerificationSession>>) {788
+ let mut active = active.lock().await;789
+ active.sas.take();790
+ active.request.take();791
+}792
+793
+/// Claim the active session for terminal emission, if one is still present.794
+///795
+/// Exactly one party (watcher or action path) wins the claim and may emit the796
+/// terminal event; the loser stays silent so Done is never overwritten by a797
+/// later local Cancelled/Failed.798
+async fn take_verification(active: &Arc<Mutex<VerificationSession>>) -> bool {799
+ let mut active = active.lock().await;800
+ let had = active.sas.is_some() || active.request.is_some();801
+ active.sas.take();802
+ active.request.take();803
+ had804
+}805
+806
+#[cfg(test)]807
+mod tests {808
+ use matrix_sdk::encryption::VerificationState;809
+ use matrix_sdk::encryption::backups::BackupState;810
+ use ruma::owned_device_id;811
+812
+ use super::*;813
+814
+ #[test]815
+ fn sdk_security_state_is_normalized() {816
+ assert_eq!(817
+ encryption_state(818
+ owned_device_id!("DEVICE"),819
+ SdkRecoveryState::Incomplete,820
+ VerificationState::Unverified,821
+ BackupState::Unknown,822
+ ),823
+ EncryptionState {824
+ device_id: owned_device_id!("DEVICE"),825
+ device_verified: false,826
+ recovery: RecoveryState::Incomplete,827
+ key_backup: KeyBackupState::Unknown,828
+ }829
+ );830
+ assert_eq!(831
+ encryption_state(832
+ owned_device_id!("DEVICE"),833
+ SdkRecoveryState::Enabled,834
+ VerificationState::Verified,835
+ BackupState::Enabled,836
+ ),837
+ EncryptionState {838
+ device_id: owned_device_id!("DEVICE"),839
+ device_verified: true,840
+ recovery: RecoveryState::Enabled,841
+ key_backup: KeyBackupState::Enabled,842
+ }843
+ );844
+ }845
+846
+ #[tokio::test]847
+ async fn comparison_actions_require_an_active_sas() {848
+ let mut controller = VerificationController::default();849
+850
+ assert_eq!(851
+ controller.confirm().await.unwrap_err().to_string(),852
+ "verification has not reached comparison"853
+ );854
+ assert_eq!(855
+ controller.reject().await.unwrap_err().to_string(),856
+ "verification has not reached comparison"857
+ );858
+ assert_eq!(859
+ controller.cancel().await.unwrap_err().to_string(),860
+ "no verification is active"861
+ );862
+ }863
+}@@ -0,0 +1,4175 @@
1
+use std::{2
+ borrow::Cow,3
+ collections::{HashMap, HashSet, VecDeque},4
+ future::Future,5
+ io::Read as _,6
+ panic::AssertUnwindSafe,7
+ sync::{8
+ Arc,9
+ atomic::{AtomicBool, Ordering},10
+ },11
+};12
+13
+use futures_util::{FutureExt, StreamExt};14
+use matrix_sdk::{15
+ Client, Room, SessionChange,16
+ media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},17
+ room::edit::EditedContent,18
+ room::reply::{EnforceThread, Reply as SdkReply},19
+ ruma::{20
+ OwnedDeviceId, OwnedEventId, OwnedRoomId, RoomId,21
+ api::client::receipt::create_receipt::v3::ReceiptType,22
+ events::{23
+ AnyGlobalAccountDataEventContent, AnyMessageLikeEventContent,24
+ GlobalAccountDataEventType, Mentions, RoomAccountDataEventType,25
+ receipt::ReceiptThread,26
+ room::message::{27
+ AddMentions, MessageType, Relation, ReplyWithinThread,28
+ RoomMessageEventContentWithoutRelation,29
+ },30
+ },31
+ serde::Raw,32
+ },33
+ send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendHandle},34
+};35
+use matrix_sdk_ui::{36
+ sync_service::{State as SyncState, SyncService},37
+ timeline::{38
+ RoomExt, Timeline, TimelineEventItemId, TimelineFocus, TimelineReadReceiptTracking,39
+ },40
+};41
+use thiserror::Error;42
+use tokio::{43
+ sync::{Mutex, mpsc, oneshot, watch},44
+ task::{AbortHandle, Id as TaskId, JoinHandle, JoinSet},45
+};46
+47
+use crate::{48
+ AccountCommand, AttachmentUpload, ClientHandle, Command, CommandErrorKind, CommandFailure,49
+ CommandId, CommandKind, ConnectionState, CreateRoomOptions, EncryptionCommand, EngineErrorKind,50
+ EngineFailure, Event, EventStream, HistoryState, InteractiveAuth, InteractiveAuthChallenge,51
+ InteractiveAuthOutcome, LifecycleCommand, MediaCommand, MediaId, MediaSize, MessageContent,52
+ MessageEditContent, MessageMentions, NotificationCommand, PowerLevelChanges, RichText,53
+ RoomCommand, RoomNotificationMode, RoomPreset, RoomVisibility, TimelineCommand, TimelineId,54
+ TypingState,55
+ encryption::{VerificationController, encryption_state},56
+ error::{CommandError, classify_sdk_error},57
+ normalise::{MediaRegistration, MediaScope, normalise_timeline, normalise_timeline_diffs},58
+ transport::{59
+ ClientQueryError, CommandEnvelope, CommandResultError, DriverPort, DriverRequest,60
+ EventSender, SendEventError, transport_pair,61
+ },62
+ upload::prepare_upload,63
+};64
+use matrix_sdk::ruma::api::{Metadata, OutgoingRequest, auth_scheme::SendAccessToken};65
+use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};66
+67
+#[derive(Clone, Debug, Eq, PartialEq)]68
+#[non_exhaustive]69
+pub struct EngineConfig {70
+ pub command_capacity: usize,71
+ pub event_capacity: usize,72
+ pub initial_history_limit: u16,73
+ pub max_media_bytes: usize,74
+ pub max_media_sources: usize,75
+}76
+77
+impl EngineConfig {78
+ #[must_use]79
+ pub const fn with_command_capacity(mut self, capacity: usize) -> Self {80
+ self.command_capacity = capacity;81
+ self82
+ }83
+84
+ #[must_use]85
+ pub const fn with_event_capacity(mut self, capacity: usize) -> Self {86
+ self.event_capacity = capacity;87
+ self88
+ }89
+90
+ #[must_use]91
+ pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {92
+ self.initial_history_limit = limit;93
+ self94
+ }95
+96
+ #[must_use]97
+ pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {98
+ self.max_media_bytes = bytes;99
+ self100
+ }101
+102
+ #[must_use]103
+ pub const fn with_max_media_sources(mut self, sources: usize) -> Self {104
+ self.max_media_sources = sources;105
+ self106
+ }107
+}108
+109
+impl Default for EngineConfig {110
+ fn default() -> Self {111
+ Self {112
+ command_capacity: 64,113
+ event_capacity: 128,114
+ initial_history_limit: 30,115
+ max_media_bytes: 20 * 1024 * 1024,116
+ max_media_sources: 2_048,117
+ }118
+ }119
+}120
+121
+#[derive(Debug, Error)]122
+pub enum EngineError {123
+ #[error("the Matrix client is not authenticated")]124
+ Unauthenticated,125
+ #[error("command, event, and media source capacities must be nonzero")]126
+ ZeroCapacity,127
+ #[error("could not build the sync service")]128
+ SyncService(#[source] matrix_sdk_ui::sync_service::Error),129
+ #[error("the event receiver has closed")]130
+ EventReceiverClosed,131
+ #[error("engine task {task} panicked")]132
+ TaskPanicked { task: String },133
+}134
+135
+impl EngineError {136
+ fn failure(&self) -> EngineFailure {137
+ EngineFailure {138
+ kind: match self {139
+ Self::EventReceiverClosed => EngineErrorKind::EventReceiverClosed,140
+ Self::Unauthenticated141
+ | Self::ZeroCapacity142
+ | Self::SyncService(_)143
+ | Self::TaskPanicked { .. } => EngineErrorKind::Other,144
+ },145
+ message: self.to_string(),146
+ }147
+ }148
+}149
+150
+type UploadWatchers = HashMap<151
+ OwnedRoomId,152
+ (153
+ AbortHandle,154
+ watch::Receiver<Option<Result<(), CommandErrorKind>>>,155
+ ),156
+>;157
+type MediaFlights = Arc<Mutex<HashMap<(MediaId, MediaSize), Vec<CommandId>>>>;158
+159
+pub struct Engine {160
+ client: Client,161
+ sync: Arc<SyncService>,162
+ port: DriverPort,163
+ rooms: HashMap<OwnedRoomId, RoomActorState>,164
+ next_room_generation: u64,165
+ room_actor_events: mpsc::UnboundedReceiver<RoomActorEvent>,166
+ room_actor_event_sender: mpsc::UnboundedSender<RoomActorEvent>,167
+ upload_watchers: UploadWatchers,168
+ tasks: TaskRegistry,169
+ session_changes: Option<tokio::sync::broadcast::Receiver<SessionChange>>,170
+ initial_history_limit: u16,171
+ room_command_capacity: usize,172
+ max_media_bytes: usize,173
+ media: Arc<Mutex<MediaRegistry>>,174
+ notification_commands: mpsc::Sender<CommandJob<NotificationCommand>>,175
+ notification_command_receiver: Option<mpsc::Receiver<CommandJob<NotificationCommand>>>,176
+ encryption_commands: mpsc::Sender<EncryptionWork>,177
+ encryption_command_receiver: Option<mpsc::Receiver<EncryptionWork>>,178
+ verification: Arc<Mutex<VerificationController>>,179
+ room_commands: mpsc::Sender<RoomJob>,180
+ room_command_receiver: Option<mpsc::Receiver<RoomJob>>,181
+ global_account_gates: HashMap<String, oneshot::Receiver<()>>,182
+ room_account_gates: HashMap<(OwnedRoomId, String), oneshot::Receiver<()>>,183
+ media_flights: MediaFlights,184
+}185
+186
+struct CommandJob<T> {187
+ id: CommandId,188
+ command: T,189
+}190
+191
+enum EncryptionWork {192
+ Command(CommandJob<EncryptionCommand>),193
+}194
+195
+struct RoomJob {196
+ id: CommandId,197
+ command: RoomCommand,198
+ reply: Option<oneshot::Sender<Result<OwnedRoomId, CommandResultError>>>,199
+}200
+201
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]202
+enum RoomMutationKey {203
+ Room(OwnedRoomId),204
+ Target(String),205
+ Direct(ruma::OwnedUserId),206
+ Independent(CommandId),207
+}208
+209
+struct RoomJobCompletion {210
+ key: RoomMutationKey,211
+ id: CommandId,212
+ reply: Option<oneshot::Sender<Result<OwnedRoomId, CommandResultError>>>,213
+ result: Result<Option<OwnedRoomId>, CommandError>,214
+}215
+216
+#[derive(Clone)]217
+enum TaskContext {218
+ Command {219
+ id: CommandId,220
+ kind: CommandKind,221
+ },222
+ Watcher(&'static str),223
+ RoomActor {224
+ room_id: OwnedRoomId,225
+ generation: u64,226
+ },227
+}228
+229
+impl TaskContext {230
+ fn label(&self) -> String {231
+ match self {232
+ Self::Command { id, kind } => format!("{kind:?} command {}", id.get()),233
+ Self::Watcher(name) => (*name).to_owned(),234
+ Self::RoomActor {235
+ room_id,236
+ generation,237
+ } => format!("room actor {room_id} generation {generation}"),238
+ }239
+ }240
+}241
+242
+struct TaskRegistry {243
+ tasks: JoinSet<()>,244
+ contexts: HashMap<TaskId, TaskContext>,245
+}246
+247
+impl TaskRegistry {248
+ fn new() -> Self {249
+ Self {250
+ tasks: JoinSet::new(),251
+ contexts: HashMap::new(),252
+ }253
+ }254
+255
+ fn is_empty(&self) -> bool {256
+ self.tasks.is_empty()257
+ }258
+259
+ fn spawn(260
+ &mut self,261
+ context: TaskContext,262
+ future: impl Future<Output = ()> + Send + 'static,263
+ ) -> AbortHandle {264
+ let handle = self.tasks.spawn(future);265
+ self.contexts.insert(handle.id(), context);266
+ handle267
+ }268
+269
+ async fn join_next(&mut self) -> Option<(TaskContext, Result<(), tokio::task::JoinError>)> {270
+ let result = self.tasks.join_next_with_id().await?;271
+ let id = match &result {272
+ Ok((id, ())) => *id,273
+ Err(error) => error.id(),274
+ };275
+ let context = self276
+ .contexts277
+ .remove(&id)278
+ .expect("every registered task has context");279
+ Some((context, result.map(|(_, ())| ())))280
+ }281
+282
+ async fn shutdown(&mut self) {283
+ self.tasks.abort_all();284
+ while self.tasks.join_next().await.is_some() {}285
+ self.contexts.clear();286
+ }287
+}288
+289
+enum RoomActorState {290
+ Opening {291
+ generation: u64,292
+ sender: mpsc::Sender<RoomActorRequest>,293
+ },294
+ Open {295
+ generation: u64,296
+ sender: mpsc::Sender<RoomActorRequest>,297
+ },298
+ Closing {299
+ generation: u64,300
+ pending: VecDeque<DeferredRoomLifecycle>,301
+ },302
+}303
+304
+enum DeferredRoomLifecycle {305
+ Open(CommandId),306
+ Close(CommandId),307
+}308
+309
+impl RoomActorState {310
+ const fn generation(&self) -> u64 {311
+ match self {312
+ Self::Opening { generation, .. }313
+ | Self::Open { generation, .. }314
+ | Self::Closing { generation, .. } => *generation,315
+ }316
+ }317
+318
+ fn sender(&self) -> Option<&mpsc::Sender<RoomActorRequest>> {319
+ match self {320
+ Self::Opening { sender, .. } | Self::Open { sender, .. } => Some(sender),321
+ Self::Closing { .. } => None,322
+ }323
+ }324
+}325
+326
+enum RoomActorEvent {327
+ Ready {328
+ room_id: OwnedRoomId,329
+ generation: u64,330
+ },331
+ Exited {332
+ room_id: OwnedRoomId,333
+ generation: u64,334
+ },335
+}336
+337
+enum EngineActivity {338
+ Request(Option<Box<DriverRequest>>),339
+ Task(Option<(TaskContext, Result<(), tokio::task::JoinError>)>),340
+ RoomActor(Option<RoomActorEvent>),341
+}342
+343
+struct MediaRegistry {344
+ entries: HashMap<MediaId, MediaRegistration>,345
+ order: VecDeque<MediaId>,346
+ limit: usize,347
+}348
+349
+impl MediaRegistry {350
+ fn new(limit: usize) -> Self {351
+ Self {352
+ entries: HashMap::new(),353
+ order: VecDeque::new(),354
+ limit,355
+ }356
+ }357
+358
+ fn insert_all(&mut self, media: Vec<MediaRegistration>) {359
+ for media in media {360
+ if !self.entries.contains_key(&media.id) {361
+ self.order.push_back(media.id.clone());362
+ }363
+ self.entries.insert(media.id.clone(), media);364
+ }365
+ while self.entries.len() > self.limit {366
+ if let Some(id) = self.order.pop_front() {367
+ self.entries.remove(&id);368
+ }369
+ }370
+ }371
+}372
+373
+enum RoomActorRequest {374
+ Opened {375
+ id: CommandId,376
+ },377
+ Execute {378
+ id: CommandId,379
+ command: TimelineCommand,380
+ },381
+ Close {382
+ id: CommandId,383
+ },384
+}385
+386
+struct RoomActor {387
+ room_id: OwnedRoomId,388
+ generation: u64,389
+ room: Room,390
+ receiver: mpsc::Receiver<RoomActorRequest>,391
+ events: EventSender,392
+ engine_events: mpsc::UnboundedSender<RoomActorEvent>,393
+ media: Arc<Mutex<MediaRegistry>>,394
+ initial_history_limit: u16,395
+ open_ids: Vec<CommandId>,396
+}397
+398
+struct TimelineSession {399
+ id: TimelineId,400
+ timeline: Arc<Timeline>,401
+ events: EventSender,402
+ watcher: JoinHandle<()>,403
+ media: Arc<Mutex<MediaRegistry>>,404
+ current_user_id: Option<ruma::OwnedUserId>,405
+ scope: MediaScope,406
+ pagination_ids: Arc<Mutex<HashSet<CommandId>>>,407
+ pagination_sender: mpsc::Sender<PaginationRequest>,408
+ pagination_worker: JoinHandle<()>,409
+ pagination_results: Option<mpsc::UnboundedSender<(CommandId, Result<(), CommandError>)>>,410
+ pagination_result_task: JoinHandle<()>,411
+ reached_start: Arc<AtomicBool>,412
+}413
+414
+struct PaginationRequest {415
+ timeline: Arc<Timeline>,416
+ events: EventSender,417
+ timeline_id: TimelineId,418
+ reached_start: Arc<AtomicBool>,419
+ limit: u16,420
+ command_id: Option<CommandId>,421
+ results: mpsc::UnboundedSender<(CommandId, Result<(), CommandError>)>,422
+}423
+424
+impl TimelineSession {425
+ async fn start(426
+ id: TimelineId,427
+ timeline: Timeline,428
+ events: EventSender,429
+ media: Arc<Mutex<MediaRegistry>>,430
+ current_user_id: Option<ruma::OwnedUserId>,431
+ initial_history_limit: u16,432
+ ) -> Result<Self, SendEventError> {433
+ let timeline = Arc::new(timeline);434
+ let scope = MediaScope::new();435
+ let watcher = start_timeline_watcher(436
+ id.clone(),437
+ timeline.clone(),438
+ events.clone(),439
+ media.clone(),440
+ current_user_id.clone(),441
+ scope.clone(),442
+ )443
+ .await?;444
+445
+ let (pagination_sender, mut pagination_receiver) = mpsc::channel::<PaginationRequest>(32);446
+ let pagination_worker = tokio::spawn(async move {447
+ while let Some(request) = pagination_receiver.recv().await {448
+ let command_id = request.command_id;449
+ let results = request.results.clone();450
+ if AssertUnwindSafe(run_pagination(request))451
+ .catch_unwind()452
+ .await453
+ .is_err()454
+ {455
+ tracing::warn!(?command_id, "pagination task panicked");456
+ if let Some(id) = command_id {457
+ let _ = results.send((458
+ id,459
+ Err(CommandError::new(460
+ CommandErrorKind::Timeline,461
+ "pagination failed",462
+ )),463
+ ));464
+ }465
+ }466
+ }467
+ });468
+ let pagination_ids = Arc::new(Mutex::new(HashSet::new()));469
+ let (pagination_results, mut result_receiver) = mpsc::unbounded_channel();470
+ let result_events = events.clone();471
+ let result_ids = pagination_ids.clone();472
+ let pagination_result_task = tokio::spawn(async move {473
+ while let Some((id, result)) = result_receiver.recv().await {474
+ if result_ids.lock().await.remove(&id) {475
+ let _ = emit_command_result(&result_events, id, CommandKind::Timeline, result)476
+ .await;477
+ }478
+ }479
+ });480
+481
+ let mut session = Self {482
+ id,483
+ timeline,484
+ events,485
+ watcher,486
+ media,487
+ current_user_id,488
+ scope,489
+ pagination_sender,490
+ pagination_worker,491
+ pagination_ids,492
+ pagination_results: Some(pagination_results),493
+ pagination_result_task,494
+ reached_start: Arc::new(AtomicBool::new(false)),495
+ };496
+ if initial_history_limit > 0 {497
+ session.paginate(initial_history_limit, None).await;498
+ }499
+ Ok(session)500
+ }501
+502
+ async fn resync(&mut self) -> Result<(), SendEventError> {503
+ self.watcher.abort();504
+ let _ = (&mut self.watcher).await;505
+ self.watcher = start_timeline_watcher(506
+ self.id.clone(),507
+ self.timeline.clone(),508
+ self.events.clone(),509
+ self.media.clone(),510
+ self.current_user_id.clone(),511
+ self.scope.clone(),512
+ )513
+ .await?;514
+ Ok(())515
+ }516
+517
+ async fn paginate(&mut self, limit: u16, command_id: Option<CommandId>) {518
+ if let Some(id) = command_id {519
+ self.pagination_ids.lock().await.insert(id);520
+ }521
+ let request = PaginationRequest {522
+ timeline: self.timeline.clone(),523
+ events: self.events.clone(),524
+ timeline_id: self.id.clone(),525
+ reached_start: self.reached_start.clone(),526
+ limit,527
+ command_id,528
+ results: self529
+ .pagination_results530
+ .as_ref()531
+ .expect("pagination session is open")532
+ .clone(),533
+ };534
+ if let Err(error) = self.pagination_sender.try_send(request) {535
+ let kind = match error {536
+ mpsc::error::TrySendError::Full(_) => CommandErrorKind::QueueFull,537
+ mpsc::error::TrySendError::Closed(_) => CommandErrorKind::TimelineNotOpen,538
+ };539
+ if let Some(id) = command_id {540
+ self.pagination_ids.lock().await.remove(&id);541
+ let _ = emit_command_result(542
+ &self.events,543
+ id,544
+ CommandKind::Timeline,545
+ Err(CommandError::new(kind, "pagination queue is unavailable")),546
+ )547
+ .await;548
+ }549
+ }550
+ }551
+552
+ async fn close(mut self, kind: CommandErrorKind, message: &'static str) {553
+ self.pagination_worker.abort();554
+ let _ = (&mut self.pagination_worker).await;555
+ drop(self.pagination_results.take());556
+ let _ = (&mut self.pagination_result_task).await;557
+ let pending = self.pagination_ids.lock().await.drain().collect::<Vec<_>>();558
+ for id in pending {559
+ let _ = emit_command_result(560
+ &self.events,561
+ id,562
+ CommandKind::Timeline,563
+ Err(CommandError::new(kind, message)),564
+ )565
+ .await;566
+ }567
+ self.watcher.abort();568
+ let _ = (&mut self.watcher).await;569
+ }570
+}571
+572
+async fn start_timeline_watcher(573
+ id: TimelineId,574
+ timeline: Arc<Timeline>,575
+ events: EventSender,576
+ media: Arc<Mutex<MediaRegistry>>,577
+ current_user_id: Option<ruma::OwnedUserId>,578
+ scope: MediaScope,579
+) -> Result<JoinHandle<()>, SendEventError> {580
+ let (items, mut stream) = timeline.subscribe().await;581
+ let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), &scope);582
+ media.lock().await.insert_all(normalised.media);583
+ events584
+ .send(Event::TimelineReset {585
+ timeline_id: id.clone(),586
+ items: normalised.items,587
+ })588
+ .await?;589
+ Ok(tokio::spawn(async move {590
+ while let Some(diffs) = stream.next().await {591
+ let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);592
+ media.lock().await.insert_all(normalised.media);593
+ if events594
+ .send(Event::TimelineChanged {595
+ timeline_id: id.clone(),596
+ diffs: normalised.diffs,597
+ })598
+ .await599
+ .is_err()600
+ {601
+ tracing::debug!(602
+ ?id,603
+ "timeline watcher stopped because the event receiver closed"604
+ );605
+ return;606
+ }607
+ }608
+ tracing::debug!(?id, "timeline watcher stopped");609
+ }))610
+}611
+612
+impl Drop for TimelineSession {613
+ fn drop(&mut self) {614
+ self.watcher.abort();615
+ self.pagination_worker.abort();616
+ self.pagination_result_task.abort();617
+ }618
+}619
+620
+impl Engine {621
+ pub async fn from_client(622
+ client: Client,623
+ config: EngineConfig,624
+ ) -> Result<(Self, ClientHandle, EventStream), EngineError> {625
+ if client.user_id().is_none() {626
+ return Err(EngineError::Unauthenticated);627
+ }628
+ if config.command_capacity == 0629
+ || config.event_capacity == 0630
+ || config.max_media_sources == 0631
+ {632
+ return Err(EngineError::ZeroCapacity);633
+ }634
+ let session_changes = client.subscribe_to_session_changes();635
+ let sync = Arc::new(636
+ SyncService::builder(client.clone())637
+ .with_offline_mode()638
+ .build()639
+ .await640
+ .map_err(EngineError::SyncService)?,641
+ );642
+ let (handle, port, events) = transport_pair(643
+ client.clone(),644
+ sync.clone(),645
+ config.command_capacity,646
+ config.event_capacity,647
+ );648
+ client.send_queue().enable_upload_progress(true);649
+ let (room_actor_event_sender, room_actor_events) = mpsc::unbounded_channel();650
+ let (notification_commands, notification_command_receiver) =651
+ mpsc::channel(config.command_capacity);652
+ let (encryption_commands, encryption_command_receiver) =653
+ mpsc::channel(config.command_capacity);654
+ let (room_commands, room_command_receiver) = mpsc::channel(config.command_capacity);655
+ Ok((656
+ Self {657
+ client,658
+ sync,659
+ port,660
+ rooms: HashMap::new(),661
+ next_room_generation: 0,662
+ room_actor_events,663
+ room_actor_event_sender,664
+ upload_watchers: HashMap::new(),665
+ tasks: TaskRegistry::new(),666
+ session_changes: Some(session_changes),667
+ initial_history_limit: config.initial_history_limit,668
+ room_command_capacity: config.command_capacity,669
+ max_media_bytes: config.max_media_bytes,670
+ media: Arc::new(Mutex::new(MediaRegistry::new(config.max_media_sources))),671
+ notification_commands,672
+ notification_command_receiver: Some(notification_command_receiver),673
+ encryption_commands,674
+ encryption_command_receiver: Some(encryption_command_receiver),675
+ verification: Arc::new(Mutex::new(VerificationController::default())),676
+ room_commands,677
+ room_command_receiver: Some(room_command_receiver),678
+ global_account_gates: HashMap::new(),679
+ room_account_gates: HashMap::new(),680
+ media_flights: Arc::new(Mutex::new(HashMap::new())),681
+ },682
+ handle,683
+ events,684
+ ))685
+ }686
+687
+ pub async fn run(mut self) -> Result<(), EngineError> {688
+ let sender = self.port.event_sender();689
+ let user_id = self690
+ .client691
+ .user_id()692
+ .expect("authentication was checked during construction")693
+ .to_owned();694
+ if sender.send(Event::Authenticated { user_id }).await.is_err() {695
+ self.shutdown().await;696
+ return Err(EngineError::EventReceiverClosed);697
+ }698
+ self.port699
+ .connection_sender()700
+ .send_replace(ConnectionState::Connecting);701
+ if sender702
+ .send(Event::ConnectionChanged(ConnectionState::Connecting))703
+ .await704
+ .is_err()705
+ {706
+ self.shutdown().await;707
+ return Err(EngineError::EventReceiverClosed);708
+ }709
+ self.start_watchers();710
+ self.start_domain_workers();711
+ self.sync.start().await;712
+ let mut terminal_error = None;713
+714
+ loop {715
+ let activity = tokio::select! {716
+ biased;717
+ _ = sender.closed() => {718
+ terminal_error = Some(EngineError::EventReceiverClosed);719
+ break;720
+ }721
+ request = self.port.recv() => EngineActivity::Request(request.map(Box::new)),722
+ completed = self.tasks.join_next(), if !self.tasks.is_empty() => {723
+ EngineActivity::Task(completed)724
+ }725
+ event = self.room_actor_events.recv() => EngineActivity::RoomActor(event),726
+ };727
+ match activity {728
+ EngineActivity::Request(Some(request)) => {729
+ let Some(envelope) = self.dispatch_request(*request).await else {730
+ continue;731
+ };732
+ let id = envelope.id;733
+ let command = envelope.command;734
+ if matches!(command, Command::Lifecycle(LifecycleCommand::Shutdown)) {735
+ let _ =736
+ emit_command_result(&sender, id, CommandKind::Lifecycle, Ok(())).await;737
+ break;738
+ }739
+ let kind = command.kind();740
+ tracing::debug!(?id, ?kind, "dispatching command");741
+ let result = self.execute(id, command).await;742
+ if Self::emit_sync_result(&sender, id, kind, result)743
+ .await744
+ .is_err()745
+ {746
+ terminal_error = Some(EngineError::EventReceiverClosed);747
+ break;748
+ }749
+ }750
+ EngineActivity::Request(None) => break,751
+ EngineActivity::Task(Some((context, result))) => {752
+ if let Err(error) = result753
+ && error.is_panic()754
+ {755
+ match context {756
+ TaskContext::Command { id, kind } => {757
+ if sender758
+ .send(Event::CommandFailed(CommandFailure {759
+ id,760
+ command: kind,761
+ kind: CommandErrorKind::Other,762
+ message: "command task panicked".to_owned(),763
+ }))764
+ .await765
+ .is_err()766
+ {767
+ terminal_error = Some(EngineError::EventReceiverClosed);768
+ break;769
+ }770
+ }771
+ context => {772
+ terminal_error = Some(EngineError::TaskPanicked {773
+ task: context.label(),774
+ });775
+ break;776
+ }777
+ }778
+ }779
+ }780
+ EngineActivity::Task(None) => {}781
+ EngineActivity::RoomActor(Some(event)) => {782
+ if let Err(error) = self.handle_room_actor_event(event).await {783
+ terminal_error = Some(error);784
+ break;785
+ }786
+ }787
+ EngineActivity::RoomActor(None) => {788
+ terminal_error = Some(EngineError::TaskPanicked {789
+ task: "room actor event channel".to_owned(),790
+ });791
+ break;792
+ }793
+ }794
+ }795
+796
+ if let Some(error) = terminal_error.as_ref() {797
+ let _ = sender.send(Event::Failed(error.failure())).await;798
+ }799
+ self.shutdown().await;800
+ let _ = sender.send(Event::Stopped).await;801
+ if let Some(error) = terminal_error {802
+ Err(error)803
+ } else {804
+ Ok(())805
+ }806
+ }807
+808
+ async fn emit_sync_result(809
+ sender: &EventSender,810
+ id: CommandId,811
+ kind: CommandKind,812
+ result: Result<bool, CommandError>,813
+ ) -> Result<(), SendEventError> {814
+ match result {815
+ Ok(true) => Ok(()),816
+ Ok(false) => emit_command_result(sender, id, kind, Ok(())).await,817
+ Err(error) => {818
+ tracing::warn!(?id, ?kind, error = ?error, "command failed");819
+ emit_command_result(sender, id, kind, Err(error)).await?;820
+ Ok(())821
+ }822
+ }823
+ }824
+825
+ fn spawn_logout(&mut self, id: CommandId) {826
+ let events = self.port.event_sender();827
+ let client = self.client.clone();828
+ self.tasks.spawn(829
+ TaskContext::Command {830
+ id,831
+ kind: CommandKind::Account,832
+ },833
+ async move {834
+ match client835
+ .logout()836
+ .await837
+ .map(|_| ())838
+ .map_err(|source| CommandError::from_sdk("logout failed", source))839
+ {840
+ Ok(()) => {841
+ let _ =842
+ emit_command_result(&events, id, CommandKind::Account, Ok(())).await;843
+ let _ = events.send(Event::LoggedOut).await;844
+ }845
+ Err(error) => {846
+ let _ = emit_command_result(&events, id, CommandKind::Account, Err(error))847
+ .await;848
+ }849
+ }850
+ },851
+ );852
+ }853
+854
+ async fn dispatch_request(&mut self, request: DriverRequest) -> Option<CommandEnvelope> {855
+ match request {856
+ DriverRequest::Command(command) => Some(command),857
+ DriverRequest::DeleteDevices {858
+ id,859
+ device_ids,860
+ auth,861
+ reply,862
+ } => {863
+ self.spawn_delete_devices(id, device_ids, auth, Some(reply));864
+ None865
+ }866
+ DriverRequest::RoomResult { id, command, reply } => {867
+ if let Err(error) = self.spawn_room_command_with_reply(id, command, Some(reply)) {868
+ tracing::warn!(?id, error = ?error, "room result request failed");869
+ }870
+ None871
+ }872
+ DriverRequest::SetGlobalAccountData {873
+ event_type,874
+ content,875
+ reply,876
+ } => {877
+ self.spawn_global_account_data_write(event_type, content, reply);878
+ None879
+ }880
+ DriverRequest::SetRoomAccountData {881
+ room_id,882
+ event_type,883
+ content,884
+ reply,885
+ } => {886
+ self.spawn_room_account_data_write(room_id, event_type, content, reply);887
+ None888
+ }889
+ }890
+ }891
+892
+ fn spawn_mark_read(&mut self, id: CommandId, timeline_id: TimelineId, event_id: OwnedEventId) {893
+ let events = self.port.event_sender();894
+ let client = self.client.clone();895
+ self.tasks.spawn(896
+ TaskContext::Command {897
+ id,898
+ kind: CommandKind::Room,899
+ },900
+ async move {901
+ let result = mark_read(&client, &timeline_id, event_id).await;902
+ let _ = emit_command_result(&events, id, CommandKind::Room, result).await;903
+ },904
+ );905
+ }906
+907
+ fn spawn_typing(&mut self, id: CommandId, room_id: OwnedRoomId, typing: bool) {908
+ let events = self.port.event_sender();909
+ let client = self.client.clone();910
+ self.tasks.spawn(911
+ TaskContext::Command {912
+ id,913
+ kind: CommandKind::Room,914
+ },915
+ async move {916
+ let result = set_typing(&client, &room_id, typing).await;917
+ let _ = emit_command_result(&events, id, CommandKind::Room, result).await;918
+ },919
+ );920
+ }921
+922
+ fn spawn_cancel_send(923
+ &mut self,924
+ id: CommandId,925
+ room_id: OwnedRoomId,926
+ transaction_id: matrix_sdk::ruma::OwnedTransactionId,927
+ ) {928
+ let events = self.port.event_sender();929
+ let client = self.client.clone();930
+ self.tasks.spawn(931
+ TaskContext::Command {932
+ id,933
+ kind: CommandKind::Room,934
+ },935
+ async move {936
+ let result = cancel_send(&client, &room_id, &transaction_id).await;937
+ let _ = emit_command_result(&events, id, CommandKind::Room, result).await;938
+ },939
+ );940
+ }941
+942
+ fn spawn_retry_send(943
+ &mut self,944
+ id: CommandId,945
+ room_id: OwnedRoomId,946
+ transaction_id: matrix_sdk::ruma::OwnedTransactionId,947
+ ) {948
+ let events = self.port.event_sender();949
+ let client = self.client.clone();950
+ self.tasks.spawn(951
+ TaskContext::Command {952
+ id,953
+ kind: CommandKind::Room,954
+ },955
+ async move {956
+ let result = retry_send(&client, &room_id, &transaction_id).await;957
+ let _ = emit_command_result(&events, id, CommandKind::Room, result).await;958
+ },959
+ );960
+ }961
+962
+ fn spawn_delete_devices(963
+ &mut self,964
+ id: CommandId,965
+ device_ids: Vec<OwnedDeviceId>,966
+ auth: Option<InteractiveAuth>,967
+ reply: Option<oneshot::Sender<Result<InteractiveAuthOutcome, CommandResultError>>>,968
+ ) {969
+ let events = self.port.event_sender();970
+ let client = self.client.clone();971
+ self.tasks.spawn(972
+ TaskContext::Command {973
+ id,974
+ kind: CommandKind::Account,975
+ },976
+ async move {977
+ let auth = match auth {978
+ Some(auth) => match auth.into_sdk(979
+ client980
+ .user_id()981
+ .expect("authentication was checked during construction"),982
+ ) {983
+ Ok(auth) => Some(auth),984
+ Err(message) => {985
+ let failure = CommandFailure {986
+ id,987
+ command: CommandKind::Account,988
+ kind: CommandErrorKind::Other,989
+ message: format!(990
+ "interactive authentication data is invalid, {message}"991
+ ),992
+ };993
+ if let Some(reply) = reply {994
+ let _ =995
+ reply.send(Err(CommandResultError::Failed(failure.clone())));996
+ } else {997
+ let _ = events.send(Event::CommandFailed(failure)).await;998
+ }999
+ return;1000
+ }1001
+ },1002
+ None => None,1003
+ };1004
+ let result = client.delete_devices(&device_ids, auth).await;1005
+1006
+ match result {1007
+ Ok(_) => {1008
+ if let Some(reply) = reply {1009
+ let _ = reply.send(Ok(InteractiveAuthOutcome::Completed));1010
+ } else {1011
+ let _ = emit_command_result(&events, id, CommandKind::Account, Ok(()))1012
+ .await;1013
+ }1014
+ }1015
+ Err(source) => {1016
+ if let Some(info) = source.as_uiaa_response() {1017
+ let challenge = InteractiveAuthChallenge::from(info);1018
+ if let Some(reply) = reply {1019
+ let _ =1020
+ reply.send(Ok(InteractiveAuthOutcome::Challenge(challenge)));1021
+ } else {1022
+ let _ = events1023
+ .send(Event::InteractiveAuthRequired {1024
+ id,1025
+ command: CommandKind::Account,1026
+ challenge: challenge.clone(),1027
+ })1028
+ .await;1029
+ let _ = events1030
+ .send(Event::CommandFailed(CommandFailure {1031
+ id,1032
+ command: CommandKind::Account,1033
+ kind: CommandErrorKind::InteractiveAuthRequired,1034
+ message: "interactive authentication is required"1035
+ .to_owned(),1036
+ }))1037
+ .await;1038
+ }1039
+ } else {1040
+ let error = CommandError::from_http("deleting devices failed", source);1041
+ let failure = CommandFailure {1042
+ id,1043
+ command: CommandKind::Account,1044
+ kind: error.kind,1045
+ message: error.to_string(),1046
+ };1047
+ if let Some(reply) = reply {1048
+ let _ =1049
+ reply.send(Err(CommandResultError::Failed(failure.clone())));1050
+ } else {1051
+ let _ = events.send(Event::CommandFailed(failure)).await;1052
+ }1053
+ }1054
+ }1055
+ }1056
+ },1057
+ );1058
+ }1059
+1060
+ fn spawn_rename_device(1061
+ &mut self,1062
+ id: CommandId,1063
+ device_id: OwnedDeviceId,1064
+ display_name: String,1065
+ ) {1066
+ let events = self.port.event_sender();1067
+ let client = self.client.clone();1068
+ self.tasks.spawn(1069
+ TaskContext::Command {1070
+ id,1071
+ kind: CommandKind::Account,1072
+ },1073
+ async move {1074
+ match client.rename_device(&device_id, &display_name).await {1075
+ Ok(_) => {1076
+ let _ = events1077
+ .send(Event::CommandSucceeded {1078
+ id,1079
+ command: CommandKind::Account,1080
+ })1081
+ .await;1082
+ }1083
+ Err(source) => {1084
+ let error = CommandError::from_http("renaming device failed", source);1085
+ let _ = events1086
+ .send(Event::CommandFailed(CommandFailure {1087
+ id,1088
+ command: CommandKind::Account,1089
+ kind: error.kind,1090
+ message: error.to_string(),1091
+ }))1092
+ .await;1093
+ }1094
+ }1095
+ },1096
+ );1097
+ }1098
+1099
+ fn spawn_global_account_data_write(1100
+ &mut self,1101
+ event_type: GlobalAccountDataEventType,1102
+ content: Raw<AnyGlobalAccountDataEventContent>,1103
+ reply: oneshot::Sender<Result<(), ClientQueryError>>,1104
+ ) {1105
+ let (done, next) = oneshot::channel();1106
+ let previous = self1107
+ .global_account_gates1108
+ .insert(event_type.to_string(), next);1109
+ let client = self.client.clone();1110
+ self.tasks.spawn(1111
+ TaskContext::Watcher("global account data writer"),1112
+ async move {1113
+ if let Some(previous) = previous {1114
+ let _ = previous.await;1115
+ }1116
+ let result = client1117
+ .account()1118
+ .set_account_data_raw(event_type, content)1119
+ .await1120
+ .map(|_| ())1121
+ .map_err(ClientQueryError::Sdk);1122
+ let _ = reply.send(result);1123
+ let _ = done.send(());1124
+ },1125
+ );1126
+ }1127
+1128
+ fn spawn_room_account_data_write(1129
+ &mut self,1130
+ room_id: OwnedRoomId,1131
+ event_type: RoomAccountDataEventType,1132
+ content: Raw<matrix_sdk::ruma::events::AnyRoomAccountDataEventContent>,1133
+ reply: oneshot::Sender<Result<(), ClientQueryError>>,1134
+ ) {1135
+ let (done, next) = oneshot::channel();1136
+ let previous = self1137
+ .room_account_gates1138
+ .insert((room_id.clone(), event_type.to_string()), next);1139
+ let client = self.client.clone();1140
+ self.tasks.spawn(1141
+ TaskContext::Watcher("room account data writer"),1142
+ async move {1143
+ if let Some(previous) = previous {1144
+ let _ = previous.await;1145
+ }1146
+ let result = match client.get_room(&room_id) {1147
+ Some(room) => room1148
+ .set_account_data_raw(event_type, content)1149
+ .await1150
+ .map(|_| ())1151
+ .map_err(ClientQueryError::Sdk),1152
+ None => Err(ClientQueryError::RoomUnknown(room_id)),1153
+ };1154
+ let _ = reply.send(result);1155
+ let _ = done.send(());1156
+ },1157
+ );1158
+ }1159
+1160
+ fn start_domain_workers(&mut self) {1161
+ let mut notification_commands = self1162
+ .notification_command_receiver1163
+ .take()1164
+ .expect("notification worker has not started");1165
+ let notification_client = self.client.clone();1166
+ let notification_events = self.port.event_sender();1167
+ self.tasks.spawn(1168
+ TaskContext::Watcher("notification command worker"),1169
+ async move {1170
+ while let Some(CommandJob { id, command }) = notification_commands.recv().await {1171
+ let result = AssertUnwindSafe(execute_notification_command(1172
+ ¬ification_client,1173
+ command,1174
+ ))1175
+ .catch_unwind()1176
+ .await1177
+ .unwrap_or_else(|_| {1178
+ Err(CommandError::new(1179
+ CommandErrorKind::Other,1180
+ "notification command panicked",1181
+ ))1182
+ });1183
+ let _ = emit_command_result(1184
+ ¬ification_events,1185
+ id,1186
+ CommandKind::Notification,1187
+ result,1188
+ )1189
+ .await;1190
+ }1191
+ },1192
+ );1193
+1194
+ let mut encryption_commands = self1195
+ .encryption_command_receiver1196
+ .take()1197
+ .expect("encryption worker has not started");1198
+ let encryption_client = self.client.clone();1199
+ let encryption_events = self.port.event_sender();1200
+ let verification = self.verification.clone();1201
+ self.tasks.spawn(1202
+ TaskContext::Watcher("encryption command worker"),1203
+ async move {1204
+ while let Some(work) = encryption_commands.recv().await {1205
+ match work {1206
+ EncryptionWork::Command(CommandJob { id, command }) => {1207
+ if matches!(1208
+ command,1209
+ EncryptionCommand::Recover(_)1210
+ | EncryptionCommand::RecoverKeyBackup(_)1211
+ ) {1212
+ let result = AssertUnwindSafe(execute_encryption_command(1213
+ &encryption_client,1214
+ &encryption_events,1215
+ &mut VerificationController::default(),1216
+ command,1217
+ ))1218
+ .catch_unwind()1219
+ .await1220
+ .unwrap_or_else(|_| {1221
+ Err(CommandError::new(1222
+ CommandErrorKind::Other,1223
+ "encryption command panicked",1224
+ ))1225
+ });1226
+ let _ = emit_command_result(1227
+ &encryption_events,1228
+ id,1229
+ CommandKind::Encryption,1230
+ result,1231
+ )1232
+ .await;1233
+ continue;1234
+ }1235
+ let is_key_backup = matches!(1236
+ command,1237
+ EncryptionCommand::CreateKeyBackup1238
+ | EncryptionCommand::EnableKeyBackup1239
+ | EncryptionCommand::DisableKeyBackup { .. }1240
+ );1241
+ let result = if is_key_backup {1242
+ AssertUnwindSafe(execute_key_backup_command(1243
+ &encryption_client,1244
+ command,1245
+ ))1246
+ .catch_unwind()1247
+ .await1248
+ .unwrap_or_else(|_| {1249
+ Err(CommandError::new(1250
+ CommandErrorKind::Other,1251
+ "encryption command panicked",1252
+ ))1253
+ })1254
+ } else {1255
+ let mut verification = verification.lock().await;1256
+ let result = AssertUnwindSafe(execute_encryption_command(1257
+ &encryption_client,1258
+ &encryption_events,1259
+ &mut verification,1260
+ command,1261
+ ))1262
+ .catch_unwind()1263
+ .await1264
+ .unwrap_or_else(|_| {1265
+ Err(CommandError::new(1266
+ CommandErrorKind::Other,1267
+ "encryption command panicked",1268
+ ))1269
+ });1270
+ drop(verification);1271
+ result1272
+ };1273
+ let _ = emit_command_result(1274
+ &encryption_events,1275
+ id,1276
+ CommandKind::Encryption,1277
+ result,1278
+ )1279
+ .await;1280
+ }1281
+ }1282
+ }1283
+ },1284
+ );1285
+1286
+ let room_commands = self1287
+ .room_command_receiver1288
+ .take()1289
+ .expect("room command worker has not started");1290
+ let room_client = self.client.clone();1291
+ let room_events = self.port.event_sender();1292
+ self.tasks.spawn(1293
+ TaskContext::Watcher("room command worker"),1294
+ run_room_command_worker(1295
+ room_client,1296
+ room_events,1297
+ room_commands,1298
+ self.room_command_capacity,1299
+ ),1300
+ );1301
+ }1302
+1303
+ fn start_watchers(&mut self) {1304
+ let mut session_changes = self1305
+ .session_changes1306
+ .take()1307
+ .expect("session watcher has not started");1308
+ let session_sender = self.port.event_sender();1309
+ self.tasks1310
+ .spawn(TaskContext::Watcher("session change watcher"), async move {1311
+ loop {1312
+ let event = match session_changes.recv().await {1313
+ Ok(SessionChange::TokensRefreshed) => Event::SessionTokensChanged,1314
+ Ok(SessionChange::UnknownToken(_)) => Event::SessionAccessRejected,1315
+ Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {1316
+ Event::SessionTokensChanged1317
+ }1318
+ Err(tokio::sync::broadcast::error::RecvError::Closed) => break,1319
+ };1320
+ if session_sender.send(event).await.is_err() {1321
+ break;1322
+ }1323
+ }1324
+ });1325
+1326
+ let mut sync_states = self.sync.state();1327
+ let state_sender = self.port.event_sender();1328
+ let connection_sender = self.port.connection_sender();1329
+ self.tasks.spawn(1330
+ TaskContext::Watcher("connection state watcher"),1331
+ async move {1332
+ while let Some(state) = sync_states.next().await {1333
+ let state = match state {1334
+ SyncState::Idle => ConnectionState::Idle,1335
+ SyncState::Running => ConnectionState::Online,1336
+ SyncState::Offline => ConnectionState::Offline,1337
+ SyncState::Terminated => ConnectionState::Stopped,1338
+ SyncState::Error(error) => {1339
+ tracing::warn!(error = %error, "sync service reported an error");1340
+ ConnectionState::Offline1341
+ }1342
+ };1343
+ connection_sender.send_replace(state);1344
+ if state_sender1345
+ .send(Event::ConnectionChanged(state))1346
+ .await1347
+ .is_err()1348
+ {1349
+ tracing::debug!(1350
+ "connection watcher stopped because the event receiver closed"1351
+ );1352
+ break;1353
+ }1354
+ }1355
+ tracing::debug!("connection watcher stopped because the sync state stream ended");1356
+ },1357
+ );1358
+1359
+ let client = self.client.clone();1360
+ let encryption_sender = self.port.event_sender();1361
+ self.tasks.spawn(1362
+ TaskContext::Watcher("encryption state watcher"),1363
+ async move {1364
+ let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {1365
+ tracing::warn!("encryption watcher stopped because the client has no device id");1366
+ return;1367
+ };1368
+ let recovery = client.encryption().recovery();1369
+ let backups = client.encryption().backups();1370
+ let mut recovery_stream = recovery.state_stream();1371
+ let mut backup_stream = backups.state_stream();1372
+ let mut verification_stream = client.encryption().verification_state();1373
+ let mut recovery_state = recovery.state();1374
+ let mut backup_state = backups.state();1375
+ let mut verification_state = verification_stream.get();1376
+ if encryption_sender1377
+ .send(Event::EncryptionChanged(encryption_state(1378
+ device_id.clone(),1379
+ recovery_state,1380
+ verification_state,1381
+ backup_state,1382
+ )))1383
+ .await1384
+ .is_err()1385
+ {1386
+ tracing::debug!(1387
+ "encryption watcher stopped because the event receiver closed"1388
+ );1389
+ return;1390
+ }1391
+ loop {1392
+ tokio::select! {1393
+ update = recovery_stream.next() => {1394
+ let Some(update) = update else {1395
+ tracing::debug!("encryption watcher stopped because tshow full diff (262144 bytes truncated)