Commit 973f8fbdaf80
tarball105 files changed+33707-0
@@ -0,0 +1 @@
1
+load flake@@ -0,0 +1,2 @@
1
+[target.wasm32-unknown-unknown]2
+rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]@@ -0,0 +1,12 @@
1
+/target2
+/crates/malice-worker/target3
+/web/node_modules4
+/web/dist5
+/web/dist-device6
+/web/src/worker/pkg7
+/web/.test-dist8
+/result9
+/.direnv10
+/.env11
+/local-data/12
+/data@@ -0,0 +1,5013 @@
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.1.0",12
+ "proc-macro2",13
+ "quote",14
+ "syn 3.0.6",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.4"128
+source = "registry+https://github.com/rust-lang/crates.io-index"129
+checksum = "4b220e7e9afc9b4ebf03f061f1a3f8e09a8aeee303bfab530bff54cfdfe311d6"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.48"152
+source = "registry+https://github.com/rust-lang/crates.io-index"153
+checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd"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.6",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 = "axum"256
+version = "0.8.9"257
+source = "registry+https://github.com/rust-lang/crates.io-index"258
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"259
+dependencies = [260
+ "axum-core",261
+ "axum-macros",262
+ "bytes",263
+ "form_urlencoded",264
+ "futures-util",265
+ "http",266
+ "http-body",267
+ "http-body-util",268
+ "hyper",269
+ "hyper-util",270
+ "itoa",271
+ "matchit",272
+ "memchr",273
+ "mime",274
+ "multer",275
+ "percent-encoding",276
+ "pin-project-lite",277
+ "serde_core",278
+ "serde_json",279
+ "serde_path_to_error",280
+ "serde_urlencoded",281
+ "sync_wrapper",282
+ "tokio",283
+ "tower",284
+ "tower-layer",285
+ "tower-service",286
+ "tracing",287
+]288
+289
+[[package]]290
+name = "axum-core"291
+version = "0.5.6"292
+source = "registry+https://github.com/rust-lang/crates.io-index"293
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"294
+dependencies = [295
+ "bytes",296
+ "futures-core",297
+ "http",298
+ "http-body",299
+ "http-body-util",300
+ "mime",301
+ "pin-project-lite",302
+ "sync_wrapper",303
+ "tower-layer",304
+ "tower-service",305
+ "tracing",306
+]307
+308
+[[package]]309
+name = "axum-macros"310
+version = "0.5.1"311
+source = "registry+https://github.com/rust-lang/crates.io-index"312
+checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"313
+dependencies = [314
+ "proc-macro2",315
+ "quote",316
+ "syn 2.0.119",317
+]318
+319
+[[package]]320
+name = "backon"321
+version = "1.6.0"322
+source = "registry+https://github.com/rust-lang/crates.io-index"323
+checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"324
+dependencies = [325
+ "fastrand",326
+ "gloo-timers",327
+ "tokio",328
+]329
+330
+[[package]]331
+name = "base64"332
+version = "0.22.1"333
+source = "registry+https://github.com/rust-lang/crates.io-index"334
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"335
+336
+[[package]]337
+name = "base64"338
+version = "0.23.1"339
+source = "registry+https://github.com/rust-lang/crates.io-index"340
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"341
+342
+[[package]]343
+name = "base64ct"344
+version = "1.8.3"345
+source = "registry+https://github.com/rust-lang/crates.io-index"346
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"347
+348
+[[package]]349
+name = "bitflags"350
+version = "2.13.2"351
+source = "registry+https://github.com/rust-lang/crates.io-index"352
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"353
+dependencies = [354
+ "serde_core",355
+]356
+357
+[[package]]358
+name = "bitmaps"359
+version = "3.2.1"360
+source = "registry+https://github.com/rust-lang/crates.io-index"361
+checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"362
+363
+[[package]]364
+name = "blake3"365
+version = "1.8.7"366
+source = "registry+https://github.com/rust-lang/crates.io-index"367
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"368
+dependencies = [369
+ "arrayvec",370
+ "cc",371
+ "cfg-if",372
+ "constant_time_eq",373
+ "cpufeatures 0.3.1",374
+]375
+376
+[[package]]377
+name = "block-buffer"378
+version = "0.10.4"379
+source = "registry+https://github.com/rust-lang/crates.io-index"380
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"381
+dependencies = [382
+ "generic-array",383
+]384
+385
+[[package]]386
+name = "block-padding"387
+version = "0.3.3"388
+source = "registry+https://github.com/rust-lang/crates.io-index"389
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"390
+dependencies = [391
+ "generic-array",392
+]393
+394
+[[package]]395
+name = "bs58"396
+version = "0.5.1"397
+source = "registry+https://github.com/rust-lang/crates.io-index"398
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"399
+dependencies = [400
+ "tinyvec",401
+]402
+403
+[[package]]404
+name = "bumpalo"405
+version = "3.20.3"406
+source = "registry+https://github.com/rust-lang/crates.io-index"407
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"408
+409
+[[package]]410
+name = "byteorder"411
+version = "1.5.0"412
+source = "registry+https://github.com/rust-lang/crates.io-index"413
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"414
+415
+[[package]]416
+name = "bytes"417
+version = "1.12.1"418
+source = "registry+https://github.com/rust-lang/crates.io-index"419
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"420
+421
+[[package]]422
+name = "bytesize"423
+version = "2.7.0"424
+source = "registry+https://github.com/rust-lang/crates.io-index"425
+checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b"426
+427
+[[package]]428
+name = "cast"429
+version = "0.3.0"430
+source = "registry+https://github.com/rust-lang/crates.io-index"431
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"432
+433
+[[package]]434
+name = "cbc"435
+version = "0.1.2"436
+source = "registry+https://github.com/rust-lang/crates.io-index"437
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"438
+dependencies = [439
+ "cipher",440
+]441
+442
+[[package]]443
+name = "cc"444
+version = "1.4.7"445
+source = "registry+https://github.com/rust-lang/crates.io-index"446
+checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"447
+dependencies = [448
+ "find-msvc-tools",449
+ "jobserver",450
+ "libc",451
+ "shlex",452
+]453
+454
+[[package]]455
+name = "cfg-if"456
+version = "1.0.5"457
+source = "registry+https://github.com/rust-lang/crates.io-index"458
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"459
+460
+[[package]]461
+name = "cfg_aliases"462
+version = "0.2.2"463
+source = "registry+https://github.com/rust-lang/crates.io-index"464
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"465
+466
+[[package]]467
+name = "chacha20"468
+version = "0.9.1"469
+source = "registry+https://github.com/rust-lang/crates.io-index"470
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"471
+dependencies = [472
+ "cfg-if",473
+ "cipher",474
+ "cpufeatures 0.2.17",475
+]476
+477
+[[package]]478
+name = "chacha20"479
+version = "0.10.2"480
+source = "registry+https://github.com/rust-lang/crates.io-index"481
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"482
+dependencies = [483
+ "cfg-if",484
+ "cpufeatures 0.3.1",485
+ "rand_core 0.10.1",486
+]487
+488
+[[package]]489
+name = "chacha20poly1305"490
+version = "0.10.1"491
+source = "registry+https://github.com/rust-lang/crates.io-index"492
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"493
+dependencies = [494
+ "aead",495
+ "chacha20 0.9.1",496
+ "cipher",497
+ "poly1305",498
+ "zeroize",499
+]500
+501
+[[package]]502
+name = "chrono"503
+version = "0.4.45"504
+source = "registry+https://github.com/rust-lang/crates.io-index"505
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"506
+dependencies = [507
+ "iana-time-zone",508
+ "js-sys",509
+ "num-traits",510
+ "serde",511
+ "wasm-bindgen",512
+ "windows-link",513
+]514
+515
+[[package]]516
+name = "cipher"517
+version = "0.4.4"518
+source = "registry+https://github.com/rust-lang/crates.io-index"519
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"520
+dependencies = [521
+ "crypto-common",522
+ "inout",523
+ "zeroize",524
+]525
+526
+[[package]]527
+name = "cmake"528
+version = "0.1.58"529
+source = "registry+https://github.com/rust-lang/crates.io-index"530
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"531
+dependencies = [532
+ "cc",533
+]534
+535
+[[package]]536
+name = "combine"537
+version = "4.6.8"538
+source = "registry+https://github.com/rust-lang/crates.io-index"539
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"540
+dependencies = [541
+ "bytes",542
+ "memchr",543
+]544
+545
+[[package]]546
+name = "compression-codecs"547
+version = "0.4.43"548
+source = "registry+https://github.com/rust-lang/crates.io-index"549
+checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9"550
+dependencies = [551
+ "compression-core",552
+ "flate2",553
+ "memchr",554
+]555
+556
+[[package]]557
+name = "compression-core"558
+version = "0.4.33"559
+source = "registry+https://github.com/rust-lang/crates.io-index"560
+checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414"561
+562
+[[package]]563
+name = "concurrent-queue"564
+version = "2.5.0"565
+source = "registry+https://github.com/rust-lang/crates.io-index"566
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"567
+dependencies = [568
+ "crossbeam-utils",569
+]570
+571
+[[package]]572
+name = "console"573
+version = "0.16.6"574
+source = "registry+https://github.com/rust-lang/crates.io-index"575
+checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3"576
+dependencies = [577
+ "encode_unicode",578
+ "libc",579
+ "windows-sys 0.61.2",580
+]581
+582
+[[package]]583
+name = "const-oid"584
+version = "0.9.6"585
+source = "registry+https://github.com/rust-lang/crates.io-index"586
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"587
+588
+[[package]]589
+name = "const_panic"590
+version = "0.2.17"591
+source = "registry+https://github.com/rust-lang/crates.io-index"592
+checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3"593
+dependencies = [594
+ "typewit",595
+]596
+597
+[[package]]598
+name = "constant_time_eq"599
+version = "0.4.2"600
+source = "registry+https://github.com/rust-lang/crates.io-index"601
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"602
+603
+[[package]]604
+name = "core-foundation"605
+version = "0.10.1"606
+source = "registry+https://github.com/rust-lang/crates.io-index"607
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"608
+dependencies = [609
+ "core-foundation-sys",610
+ "libc",611
+]612
+613
+[[package]]614
+name = "core-foundation-sys"615
+version = "0.8.7"616
+source = "registry+https://github.com/rust-lang/crates.io-index"617
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"618
+619
+[[package]]620
+name = "core_detect"621
+version = "1.0.0"622
+source = "registry+https://github.com/rust-lang/crates.io-index"623
+checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"624
+625
+[[package]]626
+name = "cpufeatures"627
+version = "0.2.17"628
+source = "registry+https://github.com/rust-lang/crates.io-index"629
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"630
+dependencies = [631
+ "libc",632
+]633
+634
+[[package]]635
+name = "cpufeatures"636
+version = "0.3.1"637
+source = "registry+https://github.com/rust-lang/crates.io-index"638
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"639
+dependencies = [640
+ "libc",641
+]642
+643
+[[package]]644
+name = "crc32fast"645
+version = "1.5.2"646
+source = "registry+https://github.com/rust-lang/crates.io-index"647
+checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"648
+dependencies = [649
+ "cfg-if",650
+]651
+652
+[[package]]653
+name = "crossbeam-utils"654
+version = "0.8.23"655
+source = "registry+https://github.com/rust-lang/crates.io-index"656
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"657
+658
+[[package]]659
+name = "crypto-common"660
+version = "0.1.7"661
+source = "registry+https://github.com/rust-lang/crates.io-index"662
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"663
+dependencies = [664
+ "generic-array",665
+ "rand_core 0.6.4",666
+ "typenum",667
+]668
+669
+[[package]]670
+name = "ctor"671
+version = "0.2.9"672
+source = "registry+https://github.com/rust-lang/crates.io-index"673
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"674
+dependencies = [675
+ "quote",676
+ "syn 2.0.119",677
+]678
+679
+[[package]]680
+name = "ctr"681
+version = "0.9.2"682
+source = "registry+https://github.com/rust-lang/crates.io-index"683
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"684
+dependencies = [685
+ "cipher",686
+]687
+688
+[[package]]689
+name = "curve25519-dalek"690
+version = "4.1.3"691
+source = "registry+https://github.com/rust-lang/crates.io-index"692
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"693
+dependencies = [694
+ "cfg-if",695
+ "cpufeatures 0.2.17",696
+ "curve25519-dalek-derive",697
+ "digest",698
+ "fiat-crypto",699
+ "rustc_version",700
+ "serde",701
+ "subtle",702
+ "zeroize",703
+]704
+705
+[[package]]706
+name = "curve25519-dalek-derive"707
+version = "0.1.1"708
+source = "registry+https://github.com/rust-lang/crates.io-index"709
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"710
+dependencies = [711
+ "proc-macro2",712
+ "quote",713
+ "syn 2.0.119",714
+]715
+716
+[[package]]717
+name = "date_header"718
+version = "1.0.5"719
+source = "registry+https://github.com/rust-lang/crates.io-index"720
+checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d"721
+722
+[[package]]723
+name = "deadpool"724
+version = "0.12.3"725
+source = "registry+https://github.com/rust-lang/crates.io-index"726
+checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"727
+dependencies = [728
+ "deadpool-runtime 0.1.4",729
+ "lazy_static",730
+ "num_cpus",731
+ "tokio",732
+]733
+734
+[[package]]735
+name = "deadpool"736
+version = "0.13.1"737
+source = "registry+https://github.com/rust-lang/crates.io-index"738
+checksum = "3e98a7e119cd347f4201e1159b19831029e203e2d8b790547708e8157b4acf1e"739
+dependencies = [740
+ "deadpool-runtime 0.3.1",741
+ "tokio",742
+]743
+744
+[[package]]745
+name = "deadpool-runtime"746
+version = "0.1.4"747
+source = "registry+https://github.com/rust-lang/crates.io-index"748
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"749
+750
+[[package]]751
+name = "deadpool-runtime"752
+version = "0.3.1"753
+source = "registry+https://github.com/rust-lang/crates.io-index"754
+checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae"755
+dependencies = [756
+ "tokio",757
+]758
+759
+[[package]]760
+name = "deadpool-sync"761
+version = "0.2.0"762
+source = "registry+https://github.com/rust-lang/crates.io-index"763
+checksum = "e385cc95d3d582c328b36d1ff90feac061102b001894b555e6b465a2e0eaabbf"764
+dependencies = [765
+ "deadpool-runtime 0.3.1",766
+]767
+768
+[[package]]769
+name = "decancer"770
+version = "3.3.3"771
+source = "registry+https://github.com/rust-lang/crates.io-index"772
+checksum = "a9244323129647178bf41ac861a2cdb9d9c81b9b09d3d0d1de9cd302b33b8a1d"773
+774
+[[package]]775
+name = "delegate-display"776
+version = "3.0.1"777
+source = "registry+https://github.com/rust-lang/crates.io-index"778
+checksum = "54e3a499943fd5180aeffcb708164ffb22125ceeccb05491b5297981d37d6249"779
+dependencies = [780
+ "impartial-ord",781
+ "itoa",782
+ "macroific 3.1.0",783
+ "proc-macro2",784
+ "quote",785
+ "syn 3.0.6",786
+]787
+788
+[[package]]789
+name = "der"790
+version = "0.7.10"791
+source = "registry+https://github.com/rust-lang/crates.io-index"792
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"793
+dependencies = [794
+ "const-oid",795
+ "zeroize",796
+]797
+798
+[[package]]799
+name = "deranged"800
+version = "0.5.8"801
+source = "registry+https://github.com/rust-lang/crates.io-index"802
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"803
+804
+[[package]]805
+name = "derivative"806
+version = "2.2.0"807
+source = "registry+https://github.com/rust-lang/crates.io-index"808
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"809
+dependencies = [810
+ "proc-macro2",811
+ "quote",812
+ "syn 1.0.109",813
+]814
+815
+[[package]]816
+name = "derive_more"817
+version = "1.0.0"818
+source = "registry+https://github.com/rust-lang/crates.io-index"819
+checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"820
+dependencies = [821
+ "derive_more-impl 1.0.0",822
+]823
+824
+[[package]]825
+name = "derive_more"826
+version = "2.1.1"827
+source = "registry+https://github.com/rust-lang/crates.io-index"828
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"829
+dependencies = [830
+ "derive_more-impl 2.1.1",831
+]832
+833
+[[package]]834
+name = "derive_more-impl"835
+version = "1.0.0"836
+source = "registry+https://github.com/rust-lang/crates.io-index"837
+checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"838
+dependencies = [839
+ "proc-macro2",840
+ "quote",841
+ "syn 2.0.119",842
+]843
+844
+[[package]]845
+name = "derive_more-impl"846
+version = "2.1.1"847
+source = "registry+https://github.com/rust-lang/crates.io-index"848
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"849
+dependencies = [850
+ "proc-macro2",851
+ "quote",852
+ "rustc_version",853
+ "syn 2.0.119",854
+ "unicode-xid",855
+]856
+857
+[[package]]858
+name = "digest"859
+version = "0.10.7"860
+source = "registry+https://github.com/rust-lang/crates.io-index"861
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"862
+dependencies = [863
+ "block-buffer",864
+ "crypto-common",865
+ "subtle",866
+]867
+868
+[[package]]869
+name = "displaydoc"870
+version = "0.2.7"871
+source = "registry+https://github.com/rust-lang/crates.io-index"872
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"873
+dependencies = [874
+ "proc-macro2",875
+ "quote",876
+ "syn 3.0.6",877
+]878
+879
+[[package]]880
+name = "dunce"881
+version = "1.0.5"882
+source = "registry+https://github.com/rust-lang/crates.io-index"883
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"884
+885
+[[package]]886
+name = "ed25519"887
+version = "2.2.3"888
+source = "registry+https://github.com/rust-lang/crates.io-index"889
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"890
+dependencies = [891
+ "pkcs8",892
+ "serde",893
+ "signature",894
+]895
+896
+[[package]]897
+name = "ed25519-dalek"898
+version = "2.2.0"899
+source = "registry+https://github.com/rust-lang/crates.io-index"900
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"901
+dependencies = [902
+ "curve25519-dalek",903
+ "ed25519",904
+ "rand_core 0.6.4",905
+ "serde",906
+ "sha2",907
+ "subtle",908
+ "zeroize",909
+]910
+911
+[[package]]912
+name = "either"913
+version = "1.18.0"914
+source = "registry+https://github.com/rust-lang/crates.io-index"915
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"916
+917
+[[package]]918
+name = "emojis"919
+version = "0.8.2"920
+source = "registry+https://github.com/rust-lang/crates.io-index"921
+checksum = "0a4d5d50b0b58df5173d8ff1192b4d1422ceae5d981b30d4b6f8ed1d673a2bc4"922
+dependencies = [923
+ "phf",924
+]925
+926
+[[package]]927
+name = "encode_unicode"928
+version = "1.0.0"929
+source = "registry+https://github.com/rust-lang/crates.io-index"930
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"931
+932
+[[package]]933
+name = "encoding_rs"934
+version = "0.8.41"935
+source = "registry+https://github.com/rust-lang/crates.io-index"936
+checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a"937
+dependencies = [938
+ "cfg-if",939
+ "core_detect",940
+ "multiversion",941
+ "multiversion_no_op",942
+ "rustversion",943
+ "scopeguard",944
+ "simdutf8",945
+]946
+947
+[[package]]948
+name = "equivalent"949
+version = "1.0.2"950
+source = "registry+https://github.com/rust-lang/crates.io-index"951
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"952
+953
+[[package]]954
+name = "errno"955
+version = "0.3.14"956
+source = "registry+https://github.com/rust-lang/crates.io-index"957
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"958
+dependencies = [959
+ "libc",960
+ "windows-sys 0.61.2",961
+]962
+963
+[[package]]964
+name = "event-listener"965
+version = "5.4.2"966
+source = "registry+https://github.com/rust-lang/crates.io-index"967
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"968
+dependencies = [969
+ "parking",970
+ "pin-project-lite",971
+]972
+973
+[[package]]974
+name = "event-listener-strategy"975
+version = "0.5.4"976
+source = "registry+https://github.com/rust-lang/crates.io-index"977
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"978
+dependencies = [979
+ "event-listener",980
+ "pin-project-lite",981
+]982
+983
+[[package]]984
+name = "eyeball"985
+version = "0.8.8"986
+source = "registry+https://github.com/rust-lang/crates.io-index"987
+checksum = "d93bd0ebf93d61d6332d3c09a96e97975968a44e19a64c947bde06e6baff383f"988
+dependencies = [989
+ "futures-core",990
+ "readlock",991
+ "readlock-tokio",992
+ "tokio",993
+ "tokio-util",994
+ "tracing",995
+]996
+997
+[[package]]998
+name = "eyeball-im"999
+version = "0.8.0"1000
+source = "registry+https://github.com/rust-lang/crates.io-index"1001
+checksum = "4790c03df183c2b46665c1a58118c04fd3e3976ec2fe16a0aa00e00c9eea7754"1002
+dependencies = [1003
+ "futures-core",1004
+ "imbl",1005
+ "tokio",1006
+ "tracing",1007
+]1008
+1009
+[[package]]1010
+name = "eyeball-im-util"1011
+version = "0.10.0"1012
+source = "registry+https://github.com/rust-lang/crates.io-index"1013
+checksum = "809fa2e8e59c25a41f5b2440aa937f1e84df6ae48b707e65f69ce259b1c44bda"1014
+dependencies = [1015
+ "arrayvec",1016
+ "eyeball-im",1017
+ "futures-core",1018
+ "imbl",1019
+ "pin-project-lite",1020
+ "smallvec",1021
+]1022
+1023
+[[package]]1024
+name = "fallible-iterator"1025
+version = "0.3.0"1026
+source = "registry+https://github.com/rust-lang/crates.io-index"1027
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"1028
+1029
+[[package]]1030
+name = "fallible-streaming-iterator"1031
+version = "0.1.9"1032
+source = "registry+https://github.com/rust-lang/crates.io-index"1033
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"1034
+1035
+[[package]]1036
+name = "fancy_constructor"1037
+version = "2.1.1"1038
+source = "registry+https://github.com/rust-lang/crates.io-index"1039
+checksum = "4a8211ab12c36b63c269e17873331dc1da196a3edb7a5c79015677ef972da34a"1040
+dependencies = [1041
+ "macroific 3.1.0",1042
+ "proc-macro2",1043
+ "quote",1044
+ "syn 3.0.6",1045
+]1046
+1047
+[[package]]1048
+name = "fastrand"1049
+version = "2.5.0"1050
+source = "registry+https://github.com/rust-lang/crates.io-index"1051
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"1052
+1053
+[[package]]1054
+name = "fiat-crypto"1055
+version = "0.2.9"1056
+source = "registry+https://github.com/rust-lang/crates.io-index"1057
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"1058
+1059
+[[package]]1060
+name = "find-msvc-tools"1061
+version = "0.1.13"1062
+source = "registry+https://github.com/rust-lang/crates.io-index"1063
+checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"1064
+1065
+[[package]]1066
+name = "flate2"1067
+version = "1.1.10"1068
+source = "registry+https://github.com/rust-lang/crates.io-index"1069
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"1070
+dependencies = [1071
+ "crc32fast",1072
+ "miniz_oxide",1073
+ "zlib-rs",1074
+]1075
+1076
+[[package]]1077
+name = "fnv"1078
+version = "1.0.7"1079
+source = "registry+https://github.com/rust-lang/crates.io-index"1080
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"1081
+1082
+[[package]]1083
+name = "foldhash"1084
+version = "0.1.5"1085
+source = "registry+https://github.com/rust-lang/crates.io-index"1086
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"1087
+1088
+[[package]]1089
+name = "form_urlencoded"1090
+version = "1.2.2"1091
+source = "registry+https://github.com/rust-lang/crates.io-index"1092
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"1093
+dependencies = [1094
+ "percent-encoding",1095
+]1096
+1097
+[[package]]1098
+name = "fs_extra"1099
+version = "1.3.0"1100
+source = "registry+https://github.com/rust-lang/crates.io-index"1101
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"1102
+1103
+[[package]]1104
+name = "futures"1105
+version = "0.3.34"1106
+source = "registry+https://github.com/rust-lang/crates.io-index"1107
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"1108
+dependencies = [1109
+ "futures-channel",1110
+ "futures-core",1111
+ "futures-executor",1112
+ "futures-io",1113
+ "futures-sink",1114
+ "futures-task",1115
+ "futures-util",1116
+]1117
+1118
+[[package]]1119
+name = "futures-channel"1120
+version = "0.3.34"1121
+source = "registry+https://github.com/rust-lang/crates.io-index"1122
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"1123
+dependencies = [1124
+ "futures-core",1125
+ "futures-sink",1126
+]1127
+1128
+[[package]]1129
+name = "futures-core"1130
+version = "0.3.34"1131
+source = "registry+https://github.com/rust-lang/crates.io-index"1132
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"1133
+1134
+[[package]]1135
+name = "futures-executor"1136
+version = "0.3.34"1137
+source = "registry+https://github.com/rust-lang/crates.io-index"1138
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"1139
+dependencies = [1140
+ "futures-core",1141
+ "futures-task",1142
+ "futures-util",1143
+]1144
+1145
+[[package]]1146
+name = "futures-io"1147
+version = "0.3.34"1148
+source = "registry+https://github.com/rust-lang/crates.io-index"1149
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"1150
+1151
+[[package]]1152
+name = "futures-macro"1153
+version = "0.3.34"1154
+source = "registry+https://github.com/rust-lang/crates.io-index"1155
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"1156
+dependencies = [1157
+ "proc-macro2",1158
+ "quote",1159
+ "syn 3.0.6",1160
+]1161
+1162
+[[package]]1163
+name = "futures-sink"1164
+version = "0.3.34"1165
+source = "registry+https://github.com/rust-lang/crates.io-index"1166
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"1167
+1168
+[[package]]1169
+name = "futures-task"1170
+version = "0.3.34"1171
+source = "registry+https://github.com/rust-lang/crates.io-index"1172
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"1173
+1174
+[[package]]1175
+name = "futures-util"1176
+version = "0.3.34"1177
+source = "registry+https://github.com/rust-lang/crates.io-index"1178
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"1179
+dependencies = [1180
+ "futures-channel",1181
+ "futures-core",1182
+ "futures-io",1183
+ "futures-macro",1184
+ "futures-sink",1185
+ "futures-task",1186
+ "memchr",1187
+ "pin-project-lite",1188
+ "slab",1189
+]1190
+1191
+[[package]]1192
+name = "fuzzy-matcher"1193
+version = "0.3.7"1194
+source = "registry+https://github.com/rust-lang/crates.io-index"1195
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"1196
+dependencies = [1197
+ "thread_local",1198
+]1199
+1200
+[[package]]1201
+name = "generator"1202
+version = "0.8.10"1203
+source = "registry+https://github.com/rust-lang/crates.io-index"1204
+checksum = "54ade96dc9003043bce7c035c85a9df5a858bfb2039c5a2e6fdf00f324f6c551"1205
+dependencies = [1206
+ "cc",1207
+ "cfg-if",1208
+ "libc",1209
+ "log",1210
+ "rustversion",1211
+ "windows-link",1212
+ "windows-result",1213
+]1214
+1215
+[[package]]1216
+name = "generic-array"1217
+version = "0.14.7"1218
+source = "registry+https://github.com/rust-lang/crates.io-index"1219
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"1220
+dependencies = [1221
+ "typenum",1222
+ "version_check",1223
+]1224
+1225
+[[package]]1226
+name = "getrandom"1227
+version = "0.2.17"1228
+source = "registry+https://github.com/rust-lang/crates.io-index"1229
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"1230
+dependencies = [1231
+ "cfg-if",1232
+ "js-sys",1233
+ "libc",1234
+ "wasi",1235
+ "wasm-bindgen",1236
+]1237
+1238
+[[package]]1239
+name = "getrandom"1240
+version = "0.3.4"1241
+source = "registry+https://github.com/rust-lang/crates.io-index"1242
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"1243
+dependencies = [1244
+ "cfg-if",1245
+ "js-sys",1246
+ "libc",1247
+ "r-efi 5.3.0",1248
+ "wasip2",1249
+ "wasm-bindgen",1250
+]1251
+1252
+[[package]]1253
+name = "getrandom"1254
+version = "0.4.3"1255
+source = "registry+https://github.com/rust-lang/crates.io-index"1256
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"1257
+dependencies = [1258
+ "cfg-if",1259
+ "js-sys",1260
+ "libc",1261
+ "r-efi 6.0.0",1262
+ "rand_core 0.10.1",1263
+ "wasm-bindgen",1264
+]1265
+1266
+[[package]]1267
+name = "gloo-timers"1268
+version = "0.3.0"1269
+source = "registry+https://github.com/rust-lang/crates.io-index"1270
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"1271
+dependencies = [1272
+ "futures-channel",1273
+ "futures-core",1274
+ "js-sys",1275
+ "wasm-bindgen",1276
+]1277
+1278
+[[package]]1279
+name = "gloo-utils"1280
+version = "0.2.0"1281
+source = "registry+https://github.com/rust-lang/crates.io-index"1282
+checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"1283
+dependencies = [1284
+ "js-sys",1285
+ "serde",1286
+ "serde_json",1287
+ "wasm-bindgen",1288
+ "web-sys",1289
+]1290
+1291
+[[package]]1292
+name = "growable-bloom-filter"1293
+version = "2.1.1"1294
+source = "registry+https://github.com/rust-lang/crates.io-index"1295
+checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1"1296
+dependencies = [1297
+ "serde",1298
+ "serde_bytes",1299
+ "serde_derive",1300
+ "xxhash-rust",1301
+]1302
+1303
+[[package]]1304
+name = "h2"1305
+version = "0.4.19"1306
+source = "registry+https://github.com/rust-lang/crates.io-index"1307
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"1308
+dependencies = [1309
+ "atomic-waker",1310
+ "bytes",1311
+ "fnv",1312
+ "futures-core",1313
+ "futures-sink",1314
+ "http",1315
+ "indexmap",1316
+ "slab",1317
+ "tokio",1318
+ "tokio-util",1319
+ "tracing",1320
+]1321
+1322
+[[package]]1323
+name = "hashbrown"1324
+version = "0.15.5"1325
+source = "registry+https://github.com/rust-lang/crates.io-index"1326
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"1327
+dependencies = [1328
+ "foldhash",1329
+]1330
+1331
+[[package]]1332
+name = "hashbrown"1333
+version = "0.17.1"1334
+source = "registry+https://github.com/rust-lang/crates.io-index"1335
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"1336
+1337
+[[package]]1338
+name = "hashlink"1339
+version = "0.10.0"1340
+source = "registry+https://github.com/rust-lang/crates.io-index"1341
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"1342
+dependencies = [1343
+ "hashbrown 0.15.5",1344
+]1345
+1346
+[[package]]1347
+name = "hermit-abi"1348
+version = "0.5.3"1349
+source = "registry+https://github.com/rust-lang/crates.io-index"1350
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"1351
+1352
+[[package]]1353
+name = "hex"1354
+version = "0.4.3"1355
+source = "registry+https://github.com/rust-lang/crates.io-index"1356
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"1357
+1358
+[[package]]1359
+name = "hkdf"1360
+version = "0.12.4"1361
+source = "registry+https://github.com/rust-lang/crates.io-index"1362
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"1363
+dependencies = [1364
+ "hmac",1365
+]1366
+1367
+[[package]]1368
+name = "hmac"1369
+version = "0.12.1"1370
+source = "registry+https://github.com/rust-lang/crates.io-index"1371
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"1372
+dependencies = [1373
+ "digest",1374
+]1375
+1376
+[[package]]1377
+name = "html5ever"1378
+version = "0.39.0"1379
+source = "registry+https://github.com/rust-lang/crates.io-index"1380
+checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"1381
+dependencies = [1382
+ "log",1383
+ "markup5ever",1384
+]1385
+1386
+[[package]]1387
+name = "http"1388
+version = "1.5.0"1389
+source = "registry+https://github.com/rust-lang/crates.io-index"1390
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"1391
+dependencies = [1392
+ "bytes",1393
+ "itoa",1394
+]1395
+1396
+[[package]]1397
+name = "http-body"1398
+version = "1.1.0"1399
+source = "registry+https://github.com/rust-lang/crates.io-index"1400
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"1401
+dependencies = [1402
+ "bytes",1403
+ "http",1404
+]1405
+1406
+[[package]]1407
+name = "http-body-util"1408
+version = "0.1.5"1409
+source = "registry+https://github.com/rust-lang/crates.io-index"1410
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"1411
+dependencies = [1412
+ "bytes",1413
+ "futures-core",1414
+ "http",1415
+ "http-body",1416
+ "pin-project-lite",1417
+]1418
+1419
+[[package]]1420
+name = "http-range-header"1421
+version = "0.4.2"1422
+source = "registry+https://github.com/rust-lang/crates.io-index"1423
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"1424
+1425
+[[package]]1426
+name = "httparse"1427
+version = "1.10.1"1428
+source = "registry+https://github.com/rust-lang/crates.io-index"1429
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"1430
+1431
+[[package]]1432
+name = "httpdate"1433
+version = "1.0.3"1434
+source = "registry+https://github.com/rust-lang/crates.io-index"1435
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"1436
+1437
+[[package]]1438
+name = "hyper"1439
+version = "1.11.1"1440
+source = "registry+https://github.com/rust-lang/crates.io-index"1441
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"1442
+dependencies = [1443
+ "atomic-waker",1444
+ "bytes",1445
+ "futures-channel",1446
+ "futures-core",1447
+ "h2",1448
+ "http",1449
+ "http-body",1450
+ "httparse",1451
+ "httpdate",1452
+ "itoa",1453
+ "pin-project-lite",1454
+ "smallvec",1455
+ "tokio",1456
+ "want",1457
+]1458
+1459
+[[package]]1460
+name = "hyper-rustls"1461
+version = "0.27.10"1462
+source = "registry+https://github.com/rust-lang/crates.io-index"1463
+checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"1464
+dependencies = [1465
+ "http",1466
+ "hyper",1467
+ "hyper-util",1468
+ "rustls",1469
+ "tokio",1470
+ "tokio-rustls",1471
+ "tower-service",1472
+]1473
+1474
+[[package]]1475
+name = "hyper-util"1476
+version = "0.1.20"1477
+source = "registry+https://github.com/rust-lang/crates.io-index"1478
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"1479
+dependencies = [1480
+ "base64 0.22.1",1481
+ "bytes",1482
+ "futures-channel",1483
+ "futures-util",1484
+ "http",1485
+ "http-body",1486
+ "hyper",1487
+ "ipnet",1488
+ "libc",1489
+ "percent-encoding",1490
+ "pin-project-lite",1491
+ "socket2",1492
+ "tokio",1493
+ "tower-service",1494
+ "tracing",1495
+]1496
+1497
+[[package]]1498
+name = "iana-time-zone"1499
+version = "0.1.65"1500
+source = "registry+https://github.com/rust-lang/crates.io-index"1501
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"1502
+dependencies = [1503
+ "android_system_properties",1504
+ "core-foundation-sys",1505
+ "iana-time-zone-haiku",1506
+ "js-sys",1507
+ "log",1508
+ "wasm-bindgen",1509
+ "windows-core",1510
+]1511
+1512
+[[package]]1513
+name = "iana-time-zone-haiku"1514
+version = "0.1.2"1515
+source = "registry+https://github.com/rust-lang/crates.io-index"1516
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"1517
+dependencies = [1518
+ "cc",1519
+]1520
+1521
+[[package]]1522
+name = "icu_collections"1523
+version = "2.3.0"1524
+source = "registry+https://github.com/rust-lang/crates.io-index"1525
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"1526
+dependencies = [1527
+ "displaydoc",1528
+ "potential_utf",1529
+ "utf8_iter",1530
+ "yoke",1531
+ "zerofrom",1532
+ "zerovec",1533
+]1534
+1535
+[[package]]1536
+name = "icu_locale_core"1537
+version = "2.3.0"1538
+source = "registry+https://github.com/rust-lang/crates.io-index"1539
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"1540
+dependencies = [1541
+ "displaydoc",1542
+ "litemap",1543
+ "tinystr",1544
+ "writeable",1545
+ "zerovec",1546
+]1547
+1548
+[[package]]1549
+name = "icu_normalizer"1550
+version = "2.3.0"1551
+source = "registry+https://github.com/rust-lang/crates.io-index"1552
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"1553
+dependencies = [1554
+ "icu_collections",1555
+ "icu_normalizer_data",1556
+ "icu_properties",1557
+ "icu_provider",1558
+ "smallvec",1559
+ "zerovec",1560
+]1561
+1562
+[[package]]1563
+name = "icu_normalizer_data"1564
+version = "2.3.0"1565
+source = "registry+https://github.com/rust-lang/crates.io-index"1566
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"1567
+1568
+[[package]]1569
+name = "icu_properties"1570
+version = "2.3.0"1571
+source = "registry+https://github.com/rust-lang/crates.io-index"1572
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"1573
+dependencies = [1574
+ "displaydoc",1575
+ "icu_collections",1576
+ "icu_locale_core",1577
+ "icu_properties_data",1578
+ "icu_provider",1579
+ "zerotrie",1580
+ "zerovec",1581
+]1582
+1583
+[[package]]1584
+name = "icu_properties_data"1585
+version = "2.3.0"1586
+source = "registry+https://github.com/rust-lang/crates.io-index"1587
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"1588
+1589
+[[package]]1590
+name = "icu_provider"1591
+version = "2.3.1"1592
+source = "registry+https://github.com/rust-lang/crates.io-index"1593
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"1594
+dependencies = [1595
+ "displaydoc",1596
+ "icu_locale_core",1597
+ "writeable",1598
+ "yoke",1599
+ "zerofrom",1600
+ "zerotrie",1601
+ "zerovec",1602
+]1603
+1604
+[[package]]1605
+name = "idna"1606
+version = "1.1.0"1607
+source = "registry+https://github.com/rust-lang/crates.io-index"1608
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"1609
+dependencies = [1610
+ "idna_adapter",1611
+ "smallvec",1612
+ "utf8_iter",1613
+]1614
+1615
+[[package]]1616
+name = "idna_adapter"1617
+version = "1.2.2"1618
+source = "registry+https://github.com/rust-lang/crates.io-index"1619
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"1620
+dependencies = [1621
+ "icu_normalizer",1622
+ "icu_properties",1623
+]1624
+1625
+[[package]]1626
+name = "imbl"1627
+version = "6.1.0"1628
+source = "registry+https://github.com/rust-lang/crates.io-index"1629
+checksum = "0fade8ae6828627ad1fa094a891eccfb25150b383047190a3648d66d06186501"1630
+dependencies = [1631
+ "archery",1632
+ "bitmaps",1633
+ "imbl-sized-chunks",1634
+ "rand_core 0.9.5",1635
+ "rand_xoshiro",1636
+ "serde",1637
+ "version_check",1638
+]1639
+1640
+[[package]]1641
+name = "imbl-sized-chunks"1642
+version = "0.1.3"1643
+source = "registry+https://github.com/rust-lang/crates.io-index"1644
+checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d"1645
+dependencies = [1646
+ "bitmaps",1647
+]1648
+1649
+[[package]]1650
+name = "impartial-ord"1651
+version = "2.0.0"1652
+source = "registry+https://github.com/rust-lang/crates.io-index"1653
+checksum = "bc680e03d1a08bdc1c01b60e0c39723430ced63d8cabd93cee0449b6ba916a25"1654
+dependencies = [1655
+ "proc-macro2",1656
+ "quote",1657
+ "syn 3.0.6",1658
+]1659
+1660
+[[package]]1661
+name = "include_dir"1662
+version = "0.7.4"1663
+source = "registry+https://github.com/rust-lang/crates.io-index"1664
+checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"1665
+dependencies = [1666
+ "include_dir_macros",1667
+]1668
+1669
+[[package]]1670
+name = "include_dir_macros"1671
+version = "0.7.4"1672
+source = "registry+https://github.com/rust-lang/crates.io-index"1673
+checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"1674
+dependencies = [1675
+ "proc-macro2",1676
+ "quote",1677
+]1678
+1679
+[[package]]1680
+name = "indexmap"1681
+version = "2.14.2"1682
+source = "registry+https://github.com/rust-lang/crates.io-index"1683
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"1684
+dependencies = [1685
+ "equivalent",1686
+ "hashbrown 0.17.1",1687
+ "serde",1688
+ "serde_core",1689
+]1690
+1691
+[[package]]1692
+name = "inout"1693
+version = "0.1.4"1694
+source = "registry+https://github.com/rust-lang/crates.io-index"1695
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"1696
+dependencies = [1697
+ "block-padding",1698
+ "generic-array",1699
+]1700
+1701
+[[package]]1702
+name = "insta"1703
+version = "1.48.0"1704
+source = "registry+https://github.com/rust-lang/crates.io-index"1705
+checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"1706
+dependencies = [1707
+ "console",1708
+ "once_cell",1709
+ "pest",1710
+ "pest_derive",1711
+ "serde",1712
+ "similar",1713
+ "tempfile",1714
+]1715
+1716
+[[package]]1717
+name = "ipnet"1718
+version = "2.12.2"1719
+source = "registry+https://github.com/rust-lang/crates.io-index"1720
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"1721
+1722
+[[package]]1723
+name = "itertools"1724
+version = "0.10.5"1725
+source = "registry+https://github.com/rust-lang/crates.io-index"1726
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"1727
+dependencies = [1728
+ "either",1729
+]1730
+1731
+[[package]]1732
+name = "itertools"1733
+version = "0.14.0"1734
+source = "registry+https://github.com/rust-lang/crates.io-index"1735
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"1736
+dependencies = [1737
+ "either",1738
+]1739
+1740
+[[package]]1741
+name = "itoa"1742
+version = "1.0.18"1743
+source = "registry+https://github.com/rust-lang/crates.io-index"1744
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"1745
+1746
+[[package]]1747
+name = "jni"1748
+version = "0.22.4"1749
+source = "registry+https://github.com/rust-lang/crates.io-index"1750
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"1751
+dependencies = [1752
+ "cfg-if",1753
+ "combine",1754
+ "jni-macros",1755
+ "jni-sys",1756
+ "log",1757
+ "simd_cesu8",1758
+ "thiserror 2.0.21",1759
+ "walkdir",1760
+ "windows-link",1761
+]1762
+1763
+[[package]]1764
+name = "jni-macros"1765
+version = "0.22.4"1766
+source = "registry+https://github.com/rust-lang/crates.io-index"1767
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"1768
+dependencies = [1769
+ "proc-macro2",1770
+ "quote",1771
+ "rustc_version",1772
+ "simd_cesu8",1773
+ "syn 2.0.119",1774
+]1775
+1776
+[[package]]1777
+name = "jni-sys"1778
+version = "0.4.1"1779
+source = "registry+https://github.com/rust-lang/crates.io-index"1780
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"1781
+dependencies = [1782
+ "jni-sys-macros",1783
+]1784
+1785
+[[package]]1786
+name = "jni-sys-macros"1787
+version = "0.4.1"1788
+source = "registry+https://github.com/rust-lang/crates.io-index"1789
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"1790
+dependencies = [1791
+ "quote",1792
+ "syn 2.0.119",1793
+]1794
+1795
+[[package]]1796
+name = "jobserver"1797
+version = "0.1.35"1798
+source = "registry+https://github.com/rust-lang/crates.io-index"1799
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"1800
+dependencies = [1801
+ "getrandom 0.4.3",1802
+ "libc",1803
+]1804
+1805
+[[package]]1806
+name = "js-sys"1807
+version = "0.3.105"1808
+source = "registry+https://github.com/rust-lang/crates.io-index"1809
+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"1810
+dependencies = [1811
+ "cfg-if",1812
+ "futures-util",1813
+ "wasm-bindgen",1814
+]1815
+1816
+[[package]]1817
+name = "js_int"1818
+version = "0.2.2"1819
+source = "registry+https://github.com/rust-lang/crates.io-index"1820
+checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a"1821
+dependencies = [1822
+ "serde",1823
+]1824
+1825
+[[package]]1826
+name = "js_option"1827
+version = "0.2.0"1828
+source = "registry+https://github.com/rust-lang/crates.io-index"1829
+checksum = "c7dd3e281add16813cf673bf74a32249b0aa0d1c8117519a17b3ada5e8552b3c"1830
+dependencies = [1831
+ "serde_core",1832
+]1833
+1834
+[[package]]1835
+name = "konst"1836
+version = "0.4.3"1837
+source = "registry+https://github.com/rust-lang/crates.io-index"1838
+checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d"1839
+dependencies = [1840
+ "const_panic",1841
+ "typewit",1842
+]1843
+1844
+[[package]]1845
+name = "language-tags"1846
+version = "0.3.2"1847
+source = "registry+https://github.com/rust-lang/crates.io-index"1848
+checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"1849
+1850
+[[package]]1851
+name = "lazy_static"1852
+version = "1.5.0"1853
+source = "registry+https://github.com/rust-lang/crates.io-index"1854
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"1855
+1856
+[[package]]1857
+name = "libc"1858
+version = "0.2.189"1859
+source = "registry+https://github.com/rust-lang/crates.io-index"1860
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"1861
+1862
+[[package]]1863
+name = "libm"1864
+version = "0.2.16"1865
+source = "registry+https://github.com/rust-lang/crates.io-index"1866
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"1867
+1868
+[[package]]1869
+name = "libsqlite3-sys"1870
+version = "0.35.0"1871
+source = "registry+https://github.com/rust-lang/crates.io-index"1872
+checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"1873
+dependencies = [1874
+ "cc",1875
+ "pkg-config",1876
+ "vcpkg",1877
+]1878
+1879
+[[package]]1880
+name = "linux-raw-sys"1881
+version = "0.12.1"1882
+source = "registry+https://github.com/rust-lang/crates.io-index"1883
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"1884
+1885
+[[package]]1886
+name = "listenfd"1887
+version = "1.0.2"1888
+source = "registry+https://github.com/rust-lang/crates.io-index"1889
+checksum = "b87bc54a4629b4294d0b3ef041b64c40c611097a677d9dc07b2c67739fe39dba"1890
+dependencies = [1891
+ "libc",1892
+ "uuid",1893
+ "winapi",1894
+]1895
+1896
+[[package]]1897
+name = "litemap"1898
+version = "0.8.3"1899
+source = "registry+https://github.com/rust-lang/crates.io-index"1900
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"1901
+1902
+[[package]]1903
+name = "lock_api"1904
+version = "0.4.14"1905
+source = "registry+https://github.com/rust-lang/crates.io-index"1906
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"1907
+dependencies = [1908
+ "scopeguard",1909
+]1910
+1911
+[[package]]1912
+name = "log"1913
+version = "0.4.34"1914
+source = "registry+https://github.com/rust-lang/crates.io-index"1915
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"1916
+1917
+[[package]]1918
+name = "loom"1919
+version = "0.7.2"1920
+source = "registry+https://github.com/rust-lang/crates.io-index"1921
+checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"1922
+dependencies = [1923
+ "cfg-if",1924
+ "generator",1925
+ "scoped-tls",1926
+ "tracing",1927
+ "tracing-subscriber",1928
+]1929
+1930
+[[package]]1931
+name = "lru-slab"1932
+version = "0.1.3"1933
+source = "registry+https://github.com/rust-lang/crates.io-index"1934
+checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"1935
+1936
+[[package]]1937
+name = "macltok"1938
+version = "0.1.0"1939
+dependencies = [1940
+ "futures-util",1941
+ "getrandom 0.2.17",1942
+ "getrandom 0.3.4",1943
+ "getrandom 0.4.3",1944
+ "http",1945
+ "imbl",1946
+ "js-sys",1947
+ "matrix-sdk",1948
+ "matrix-sdk-crypto",1949
+ "matrix-sdk-store-encryption",1950
+ "matrix-sdk-test",1951
+ "matrix-sdk-ui",1952
+ "matrix_indexed_db_futures",1953
+ "mime",1954
+ "reqwest",1955
+ "ruma",1956
+ "serde",1957
+ "serde_json",1958
+ "sha2",1959
+ "thiserror 2.0.21",1960
+ "tokio",1961
+ "tracing",1962
+ "url",1963
+ "uuid",1964
+ "wasm-bindgen",1965
+ "wasm-bindgen-futures",1966
+ "web-sys",1967
+ "wiremock",1968
+]1969
+1970
+[[package]]1971
+name = "macroific"1972
+version = "2.0.0"1973
+source = "registry+https://github.com/rust-lang/crates.io-index"1974
+checksum = "89f276537b4b8f981bf1c13d79470980f71134b7bdcc5e6e911e910e556b0285"1975
+dependencies = [1976
+ "macroific_attr_parse 2.0.0",1977
+ "macroific_core 2.0.0",1978
+ "macroific_macro 2.0.0",1979
+]1980
+1981
+[[package]]1982
+name = "macroific"1983
+version = "3.1.0"1984
+source = "registry+https://github.com/rust-lang/crates.io-index"1985
+checksum = "cc8ab95c98294764c0f1eacebdb5b027849201b71fbc383871fe991a1de7eb83"1986
+dependencies = [1987
+ "macroific_attr_parse 3.1.0",1988
+ "macroific_core 3.1.0",1989
+ "macroific_macro 3.1.0",1990
+]1991
+1992
+[[package]]1993
+name = "macroific_attr_parse"1994
+version = "2.0.0"1995
+source = "registry+https://github.com/rust-lang/crates.io-index"1996
+checksum = "ad4023761b45fcd36abed8fb7ae6a80456b0a38102d55e89a57d9a594a236be9"1997
+dependencies = [1998
+ "proc-macro2",1999
+ "quote",2000
+ "sealed 0.6.0",2001
+ "syn 2.0.119",2002
+]2003
+2004
+[[package]]2005
+name = "macroific_attr_parse"2006
+version = "3.1.0"2007
+source = "registry+https://github.com/rust-lang/crates.io-index"2008
+checksum = "bf9b7bdb92825e6a507392b12f0570ade17480be20f4ca951b9071b40deba1d1"2009
+dependencies = [2010
+ "proc-macro2",2011
+ "quote",2012
+ "sealed 0.7.0",2013
+ "syn 3.0.6",2014
+]2015
+2016
+[[package]]2017
+name = "macroific_core"2018
+version = "2.0.0"2019
+source = "registry+https://github.com/rust-lang/crates.io-index"2020
+checksum = "d0a7594d3c14916fa55bef7e9d18c5daa9ed410dd37504251e4b75bbdeec33e3"2021
+dependencies = [2022
+ "proc-macro2",2023
+ "quote",2024
+ "sealed 0.6.0",2025
+ "syn 2.0.119",2026
+]2027
+2028
+[[package]]2029
+name = "macroific_core"2030
+version = "3.1.0"2031
+source = "registry+https://github.com/rust-lang/crates.io-index"2032
+checksum = "edc880da2bc00d6d87f748805e242d670e248d1b9b6fcef3190503210b6cf056"2033
+dependencies = [2034
+ "proc-macro2",2035
+ "quote",2036
+ "sealed 0.7.0",2037
+ "syn 3.0.6",2038
+]2039
+2040
+[[package]]2041
+name = "macroific_macro"2042
+version = "2.0.0"2043
+source = "registry+https://github.com/rust-lang/crates.io-index"2044
+checksum = "4da6f2ed796261b0a74e2b52b42c693bb6dee1effba3a482c49592659f824b3b"2045
+dependencies = [2046
+ "macroific_attr_parse 2.0.0",2047
+ "macroific_core 2.0.0",2048
+ "proc-macro2",2049
+ "quote",2050
+ "syn 2.0.119",2051
+]2052
+2053
+[[package]]2054
+name = "macroific_macro"2055
+version = "3.1.0"2056
+source = "registry+https://github.com/rust-lang/crates.io-index"2057
+checksum = "201faf668c264432727fb64e44407bcfcf1f5f8153bf4ec248eb6bbe4a39b144"2058
+dependencies = [2059
+ "macroific_attr_parse 3.1.0",2060
+ "macroific_core 3.1.0",2061
+ "proc-macro2",2062
+ "quote",2063
+ "syn 3.0.6",2064
+]2065
+2066
+[[package]]2067
+name = "malice"2068
+version = "0.1.0"2069
+dependencies = [2070
+ "axum",2071
+ "futures-util",2072
+ "hex",2073
+ "http-body-util",2074
+ "listenfd",2075
+ "macltok",2076
+ "malice-core",2077
+ "matrix-sdk",2078
+ "rand_core 0.6.4",2079
+ "reqwest",2080
+ "ruma",2081
+ "serde",2082
+ "serde_json",2083
+ "sha2",2084
+ "tempfile",2085
+ "tokio",2086
+ "tower",2087
+ "tower-http",2088
+ "tracing",2089
+ "tracing-subscriber",2090
+ "wiremock",2091
+]2092
+2093
+[[package]]2094
+name = "malice-core"2095
+version = "0.1.0"2096
+dependencies = [2097
+ "futures-util",2098
+ "http",2099
+ "macltok",2100
+ "ruma",2101
+ "serde",2102
+ "serde_json",2103
+]2104
+2105
+[[package]]2106
+name = "maplit"2107
+version = "1.0.2"2108
+source = "registry+https://github.com/rust-lang/crates.io-index"2109
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"2110
+2111
+[[package]]2112
+name = "markup5ever"2113
+version = "0.39.0"2114
+source = "registry+https://github.com/rust-lang/crates.io-index"2115
+checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"2116
+dependencies = [2117
+ "log",2118
+ "tendril",2119
+ "web_atoms",2120
+]2121
+2122
+[[package]]2123
+name = "matchers"2124
+version = "0.2.0"2125
+source = "registry+https://github.com/rust-lang/crates.io-index"2126
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"2127
+dependencies = [2128
+ "regex-automata",2129
+]2130
+2131
+[[package]]2132
+name = "matchit"2133
+version = "0.8.4"2134
+source = "registry+https://github.com/rust-lang/crates.io-index"2135
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"2136
+2137
+[[package]]2138
+name = "matrix-pickle"2139
+version = "0.2.3"2140
+source = "registry+https://github.com/rust-lang/crates.io-index"2141
+checksum = "b3d65d46b7379dd0afa4a42f9b2269821d31afdee0111b5e0d74e3bee03553a0"2142
+dependencies = [2143
+ "matrix-pickle-derive",2144
+ "thiserror 2.0.21",2145
+]2146
+2147
+[[package]]2148
+name = "matrix-pickle-derive"2149
+version = "0.2.3"2150
+source = "registry+https://github.com/rust-lang/crates.io-index"2151
+checksum = "414b5e4c34009f2bc3fe35dd018f25755ca38858096574841c7332f99e2c7e77"2152
+dependencies = [2153
+ "proc-macro-crate",2154
+ "proc-macro2",2155
+ "quote",2156
+ "syn 2.0.119",2157
+]2158
+2159
+[[package]]2160
+name = "matrix-sdk"2161
+version = "0.18.0"2162
+source = "registry+https://github.com/rust-lang/crates.io-index"2163
+checksum = "7083d580527511ac5d9369e03b9f2b20902e76949f1b3964051f978e4d3756ae"2164
+dependencies = [2165
+ "anymap2",2166
+ "aquamarine",2167
+ "as_variant",2168
+ "assert_matches2",2169
+ "async-channel",2170
+ "async-once-cell",2171
+ "async-stream",2172
+ "async-trait",2173
+ "backon",2174
+ "bytes",2175
+ "bytesize",2176
+ "cfg-if",2177
+ "event-listener",2178
+ "eyeball",2179
+ "eyeball-im",2180
+ "futures-core",2181
+ "futures-util",2182
+ "gloo-timers",2183
+ "http",2184
+ "imbl",2185
+ "indexmap",2186
+ "itertools 0.14.0",2187
+ "js_int",2188
+ "language-tags",2189
+ "matrix-sdk-base",2190
+ "matrix-sdk-common",2191
+ "matrix-sdk-indexeddb",2192
+ "matrix-sdk-sqlite",2193
+ "matrix-sdk-test",2194
+ "mime",2195
+ "mime2ext",2196
+ "oauth2",2197
+ "oauth2-reqwest",2198
+ "percent-encoding",2199
+ "pin-project-lite",2200
+ "reqwest",2201
+ "ruma",2202
+ "rustls",2203
+ "rustls-native-certs",2204
+ "rustls-pki-types",2205
+ "serde",2206
+ "serde_html_form",2207
+ "serde_json",2208
+ "sha2",2209
+ "tempfile",2210
+ "thiserror 2.0.21",2211
+ "tokio",2212
+ "tokio-stream",2213
+ "tokio-util",2214
+ "tracing",2215
+ "url",2216
+ "urlencoding",2217
+ "vodozemac",2218
+ "webpki-roots",2219
+ "wiremock",2220
+ "zeroize",2221
+]2222
+2223
+[[package]]2224
+name = "matrix-sdk-base"2225
+version = "0.18.0"2226
+source = "registry+https://github.com/rust-lang/crates.io-index"2227
+checksum = "e09a917eb1f7643d9d9a06b2f131e2ceb39df6910a7b830dac18bfd6db37a1f5"2228
+dependencies = [2229
+ "as_variant",2230
+ "assert_matches",2231
+ "assert_matches2",2232
+ "async-trait",2233
+ "bitflags",2234
+ "decancer",2235
+ "eyeball",2236
+ "eyeball-im",2237
+ "futures-util",2238
+ "growable-bloom-filter",2239
+ "http",2240
+ "matrix-sdk-common",2241
+ "matrix-sdk-crypto",2242
+ "matrix-sdk-store-encryption",2243
+ "matrix-sdk-test",2244
+ "regex",2245
+ "ruma",2246
+ "serde",2247
+ "serde_json",2248
+ "thiserror 2.0.21",2249
+ "tokio",2250
+ "tracing",2251
+ "unicode-normalization",2252
+]2253
+2254
+[[package]]2255
+name = "matrix-sdk-common"2256
+version = "0.18.0"2257
+source = "registry+https://github.com/rust-lang/crates.io-index"2258
+checksum = "2b9d1e0fee0f090180ef9457034adc547e80de9e6e3eb2e63aaac68a8476f836"2259
+dependencies = [2260
+ "eyeball-im",2261
+ "futures-core",2262
+ "futures-executor",2263
+ "futures-util",2264
+ "gloo-timers",2265
+ "imbl",2266
+ "ruma",2267
+ "serde",2268
+ "serde_json",2269
+ "thiserror 2.0.21",2270
+ "tokio",2271
+ "tracing",2272
+ "tracing-subscriber",2273
+ "wasm-bindgen",2274
+ "wasm-bindgen-futures",2275
+ "web-sys",2276
+]2277
+2278
+[[package]]2279
+name = "matrix-sdk-crypto"2280
+version = "0.18.0"2281
+source = "registry+https://github.com/rust-lang/crates.io-index"2282
+checksum = "c54afd2a326f51c13a6ad44ec86315a688fffeb3f1e287fb343e0e1836a3bdaf"2283
+dependencies = [2284
+ "aes",2285
+ "aquamarine",2286
+ "as_variant",2287
+ "async-trait",2288
+ "bs58",2289
+ "byteorder",2290
+ "cfg-if",2291
+ "ctr",2292
+ "eyeball",2293
+ "futures-core",2294
+ "futures-util",2295
+ "hkdf",2296
+ "hmac",2297
+ "itertools 0.14.0",2298
+ "js_option",2299
+ "matrix-sdk-common",2300
+ "matrix-sdk-test",2301
+ "pbkdf2",2302
+ "rand 0.10.3",2303
+ "rmp-serde",2304
+ "ruma",2305
+ "serde",2306
+ "serde_json",2307
+ "sha2",2308
+ "subtle",2309
+ "thiserror 2.0.21",2310
+ "time",2311
+ "tokio",2312
+ "tokio-stream",2313
+ "tracing",2314
+ "ulid",2315
+ "url",2316
+ "vodozemac",2317
+ "zeroize",2318
+]2319
+2320
+[[package]]2321
+name = "matrix-sdk-indexeddb"2322
+version = "0.18.0"2323
+source = "registry+https://github.com/rust-lang/crates.io-index"2324
+checksum = "fef37395fffb7c916f7109ab0d16d8ca599403dd8164d08c0d966176b66ede47"2325
+dependencies = [2326
+ "async-trait",2327
+ "base64 0.22.1",2328
+ "futures-util",2329
+ "getrandom 0.4.3",2330
+ "gloo-utils",2331
+ "growable-bloom-filter",2332
+ "hkdf",2333
+ "js-sys",2334
+ "matrix-sdk-base",2335
+ "matrix-sdk-crypto",2336
+ "matrix-sdk-store-encryption",2337
+ "matrix_indexed_db_futures",2338
+ "rmp-serde",2339
+ "ruma",2340
+ "serde",2341
+ "serde-wasm-bindgen",2342
+ "serde_json",2343
+ "sha2",2344
+ "thiserror 2.0.21",2345
+ "tokio",2346
+ "tracing",2347
+ "uuid",2348
+ "wasm-bindgen",2349
+ "web-sys",2350
+ "zeroize",2351
+]2352
+2353
+[[package]]2354
+name = "matrix-sdk-sqlite"2355
+version = "0.18.0"2356
+source = "registry+https://github.com/rust-lang/crates.io-index"2357
+checksum = "a49133429271005745f8d5a05362d2ba6567274777250236799927ca30dc0670"2358
+dependencies = [2359
+ "as_variant",2360
+ "async-trait",2361
+ "deadpool 0.13.1",2362
+ "deadpool-sync",2363
+ "itertools 0.14.0",2364
+ "matrix-sdk-base",2365
+ "matrix-sdk-crypto",2366
+ "matrix-sdk-store-encryption",2367
+ "num_cpus",2368
+ "rmp-serde",2369
+ "ruma",2370
+ "rusqlite",2371
+ "serde",2372
+ "serde_json",2373
+ "serde_path_to_error",2374
+ "thiserror 2.0.21",2375
+ "tokio",2376
+ "tracing",2377
+ "vodozemac",2378
+ "zeroize",2379
+]2380
+2381
+[[package]]2382
+name = "matrix-sdk-store-encryption"2383
+version = "0.18.0"2384
+source = "registry+https://github.com/rust-lang/crates.io-index"2385
+checksum = "2f48f304e553fb6200b1d7d1f77a88fd182076d4b25624e9dbfa42d6a37de35e"2386
+dependencies = [2387
+ "base64 0.22.1",2388
+ "blake3",2389
+ "chacha20poly1305",2390
+ "getrandom 0.2.17",2391
+ "getrandom 0.4.3",2392
+ "hmac",2393
+ "pbkdf2",2394
+ "rand 0.10.3",2395
+ "rmp-serde",2396
+ "serde",2397
+ "serde_json",2398
+ "sha2",2399
+ "thiserror 2.0.21",2400
+ "zeroize",2401
+]2402
+2403
+[[package]]2404
+name = "matrix-sdk-test"2405
+version = "0.18.0"2406
+source = "registry+https://github.com/rust-lang/crates.io-index"2407
+checksum = "8023d4315fff35598c2d4a955920c1273c6aaab4dcd9ed405238a882581649bf"2408
+dependencies = [2409
+ "as_variant",2410
+ "ctor",2411
+ "getrandom 0.4.3",2412
+ "http",2413
+ "insta",2414
+ "matrix-sdk-common",2415
+ "matrix-sdk-test-macros",2416
+ "ruma",2417
+ "serde",2418
+ "serde_json",2419
+ "sha2",2420
+ "tokio",2421
+ "tracing-subscriber",2422
+ "vodozemac",2423
+ "wasm-bindgen-test",2424
+ "wiremock",2425
+]2426
+2427
+[[package]]2428
+name = "matrix-sdk-test-macros"2429
+version = "0.18.0"2430
+source = "registry+https://github.com/rust-lang/crates.io-index"2431
+checksum = "d97d6a0e5b1401c530b3c5d18cb98257d255d396bacf3903beda6718a2f4c4c5"2432
+dependencies = [2433
+ "quote",2434
+ "syn 2.0.119",2435
+]2436
+2437
+[[package]]2438
+name = "matrix-sdk-ui"2439
+version = "0.18.0"2440
+source = "registry+https://github.com/rust-lang/crates.io-index"2441
+checksum = "6d1f2b8a8cf57164d7cee4eb61c0b9f074b2e11d766a9391b7db0b9f2cbd83d8"2442
+dependencies = [2443
+ "as_variant",2444
+ "async-rx",2445
+ "async-stream",2446
+ "async_cell",2447
+ "bitflags",2448
+ "chrono",2449
+ "emojis",2450
+ "eyeball",2451
+ "eyeball-im",2452
+ "eyeball-im-util",2453
+ "futures-core",2454
+ "futures-util",2455
+ "fuzzy-matcher",2456
+ "growable-bloom-filter",2457
+ "imbl",2458
+ "indexmap",2459
+ "itertools 0.14.0",2460
+ "matrix-sdk",2461
+ "matrix-sdk-base",2462
+ "matrix-sdk-common",2463
+ "mime",2464
+ "pin-project-lite",2465
+ "ruma",2466
+ "serde",2467
+ "serde_json",2468
+ "thiserror 2.0.21",2469
+ "tokio",2470
+ "tokio-stream",2471
+ "tracing",2472
+ "unicode-normalization",2473
+ "unicode-segmentation",2474
+]2475
+2476
+[[package]]2477
+name = "matrix_indexed_db_futures"2478
+version = "0.7.0"2479
+source = "registry+https://github.com/rust-lang/crates.io-index"2480
+checksum = "245ff6a224b4df7b0c90dda2dd5a6eb46112708d49e8bdd8b007fccb09fea8e4"2481
+dependencies = [2482
+ "accessory",2483
+ "cfg-if",2484
+ "delegate-display",2485
+ "derive_more 2.1.1",2486
+ "fancy_constructor",2487
+ "futures-core",2488
+ "js-sys",2489
+ "matrix_indexed_db_futures_macros_internal",2490
+ "sealed 0.6.0",2491
+ "serde",2492
+ "serde-wasm-bindgen",2493
+ "smallvec",2494
+ "thiserror 2.0.21",2495
+ "tokio",2496
+ "wasm-bindgen",2497
+ "wasm-bindgen-futures",2498
+ "wasm_evt_listener",2499
+ "web-sys",2500
+ "web-time",2501
+]2502
+2503
+[[package]]2504
+name = "matrix_indexed_db_futures_macros_internal"2505
+version = "1.0.0"2506
+source = "registry+https://github.com/rust-lang/crates.io-index"2507
+checksum = "b428aee5c0fe9e5babd29e99d289b7f64718c444989aac0442d1fd6d3e3f66d1"2508
+dependencies = [2509
+ "macroific 2.0.0",2510
+ "proc-macro2",2511
+ "quote",2512
+ "syn 2.0.119",2513
+]2514
+2515
+[[package]]2516
+name = "memchr"2517
+version = "2.8.3"2518
+source = "registry+https://github.com/rust-lang/crates.io-index"2519
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"2520
+2521
+[[package]]2522
+name = "mime"2523
+version = "0.3.17"2524
+source = "registry+https://github.com/rust-lang/crates.io-index"2525
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"2526
+2527
+[[package]]2528
+name = "mime2ext"2529
+version = "0.1.54"2530
+source = "registry+https://github.com/rust-lang/crates.io-index"2531
+checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc"2532
+2533
+[[package]]2534
+name = "mime_guess"2535
+version = "2.0.5"2536
+source = "registry+https://github.com/rust-lang/crates.io-index"2537
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"2538
+dependencies = [2539
+ "mime",2540
+ "unicase",2541
+]2542
+2543
+[[package]]2544
+name = "minicov"2545
+version = "0.3.8"2546
+source = "registry+https://github.com/rust-lang/crates.io-index"2547
+checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"2548
+dependencies = [2549
+ "cc",2550
+ "walkdir",2551
+]2552
+2553
+[[package]]2554
+name = "miniz_oxide"2555
+version = "0.9.1"2556
+source = "registry+https://github.com/rust-lang/crates.io-index"2557
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"2558
+dependencies = [2559
+ "adler2",2560
+ "simd-adler32",2561
+]2562
+2563
+[[package]]2564
+name = "mio"2565
+version = "1.2.3"2566
+source = "registry+https://github.com/rust-lang/crates.io-index"2567
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"2568
+dependencies = [2569
+ "libc",2570
+ "wasi",2571
+ "windows-sys 0.61.2",2572
+]2573
+2574
+[[package]]2575
+name = "multer"2576
+version = "3.1.0"2577
+source = "registry+https://github.com/rust-lang/crates.io-index"2578
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"2579
+dependencies = [2580
+ "bytes",2581
+ "encoding_rs",2582
+ "futures-util",2583
+ "http",2584
+ "httparse",2585
+ "memchr",2586
+ "mime",2587
+ "spin",2588
+ "version_check",2589
+]2590
+2591
+[[package]]2592
+name = "multiversion"2593
+version = "0.9.0"2594
+source = "registry+https://github.com/rust-lang/crates.io-index"2595
+checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c"2596
+dependencies = [2597
+ "multiversion-macros",2598
+]2599
+2600
+[[package]]2601
+name = "multiversion-macros"2602
+version = "0.9.0"2603
+source = "registry+https://github.com/rust-lang/crates.io-index"2604
+checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83"2605
+dependencies = [2606
+ "proc-macro2",2607
+ "quote",2608
+ "rustversion",2609
+ "syn 3.0.6",2610
+]2611
+2612
+[[package]]2613
+name = "multiversion_no_op"2614
+version = "1.0.0"2615
+source = "registry+https://github.com/rust-lang/crates.io-index"2616
+checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"2617
+2618
+[[package]]2619
+name = "new_debug_unreachable"2620
+version = "1.0.6"2621
+source = "registry+https://github.com/rust-lang/crates.io-index"2622
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"2623
+2624
+[[package]]2625
+name = "nu-ansi-term"2626
+version = "0.50.3"2627
+source = "registry+https://github.com/rust-lang/crates.io-index"2628
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"2629
+dependencies = [2630
+ "windows-sys 0.61.2",2631
+]2632
+2633
+[[package]]2634
+name = "num-conv"2635
+version = "0.2.2"2636
+source = "registry+https://github.com/rust-lang/crates.io-index"2637
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"2638
+2639
+[[package]]2640
+name = "num-traits"2641
+version = "0.2.19"2642
+source = "registry+https://github.com/rust-lang/crates.io-index"2643
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"2644
+dependencies = [2645
+ "autocfg",2646
+ "libm",2647
+]2648
+2649
+[[package]]2650
+name = "num_cpus"2651
+version = "1.17.0"2652
+source = "registry+https://github.com/rust-lang/crates.io-index"2653
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"2654
+dependencies = [2655
+ "hermit-abi",2656
+ "libc",2657
+]2658
+2659
+[[package]]2660
+name = "oauth2"2661
+version = "5.0.0"2662
+source = "registry+https://github.com/rust-lang/crates.io-index"2663
+checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"2664
+dependencies = [2665
+ "base64 0.22.1",2666
+ "chrono",2667
+ "getrandom 0.2.17",2668
+ "http",2669
+ "rand 0.8.8",2670
+ "serde",2671
+ "serde_json",2672
+ "serde_path_to_error",2673
+ "sha2",2674
+ "thiserror 1.0.69",2675
+ "url",2676
+]2677
+2678
+[[package]]2679
+name = "oauth2-reqwest"2680
+version = "0.1.0-alpha.3"2681
+source = "registry+https://github.com/rust-lang/crates.io-index"2682
+checksum = "234fb5c965bbce983ee5de636a7a51d6a3223da8067ea02f9ab2d2d78ac08be2"2683
+dependencies = [2684
+ "oauth2",2685
+ "reqwest",2686
+]2687
+2688
+[[package]]2689
+name = "once_cell"2690
+version = "1.21.4"2691
+source = "registry+https://github.com/rust-lang/crates.io-index"2692
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"2693
+2694
+[[package]]2695
+name = "oorandom"2696
+version = "11.1.5"2697
+source = "registry+https://github.com/rust-lang/crates.io-index"2698
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"2699
+2700
+[[package]]2701
+name = "opaque-debug"2702
+version = "0.3.1"2703
+source = "registry+https://github.com/rust-lang/crates.io-index"2704
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"2705
+2706
+[[package]]2707
+name = "openssl-probe"2708
+version = "0.2.1"2709
+source = "registry+https://github.com/rust-lang/crates.io-index"2710
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"2711
+2712
+[[package]]2713
+name = "parking"2714
+version = "2.2.1"2715
+source = "registry+https://github.com/rust-lang/crates.io-index"2716
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"2717
+2718
+[[package]]2719
+name = "parking_lot"2720
+version = "0.12.5"2721
+source = "registry+https://github.com/rust-lang/crates.io-index"2722
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"2723
+dependencies = [2724
+ "lock_api",2725
+ "parking_lot_core",2726
+]2727
+2728
+[[package]]2729
+name = "parking_lot_core"2730
+version = "0.9.12"2731
+source = "registry+https://github.com/rust-lang/crates.io-index"2732
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"2733
+dependencies = [2734
+ "cfg-if",2735
+ "libc",2736
+ "redox_syscall",2737
+ "smallvec",2738
+ "windows-link",2739
+]2740
+2741
+[[package]]2742
+name = "pbkdf2"2743
+version = "0.12.2"2744
+source = "registry+https://github.com/rust-lang/crates.io-index"2745
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"2746
+dependencies = [2747
+ "digest",2748
+]2749
+2750
+[[package]]2751
+name = "percent-encoding"2752
+version = "2.3.2"2753
+source = "registry+https://github.com/rust-lang/crates.io-index"2754
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"2755
+2756
+[[package]]2757
+name = "pest"2758
+version = "2.9.2"2759
+source = "registry+https://github.com/rust-lang/crates.io-index"2760
+checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8"2761
+dependencies = [2762
+ "memchr",2763
+ "ucd-trie",2764
+]2765
+2766
+[[package]]2767
+name = "pest_derive"2768
+version = "2.9.2"2769
+source = "registry+https://github.com/rust-lang/crates.io-index"2770
+checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf"2771
+dependencies = [2772
+ "pest",2773
+ "pest_generator",2774
+]2775
+2776
+[[package]]2777
+name = "pest_generator"2778
+version = "2.9.2"2779
+source = "registry+https://github.com/rust-lang/crates.io-index"2780
+checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109"2781
+dependencies = [2782
+ "pest",2783
+ "pest_meta",2784
+ "proc-macro2",2785
+ "quote",2786
+ "syn 2.0.119",2787
+]2788
+2789
+[[package]]2790
+name = "pest_meta"2791
+version = "2.9.2"2792
+source = "registry+https://github.com/rust-lang/crates.io-index"2793
+checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb"2794
+dependencies = [2795
+ "pest",2796
+]2797
+2798
+[[package]]2799
+name = "phf"2800
+version = "0.13.1"2801
+source = "registry+https://github.com/rust-lang/crates.io-index"2802
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"2803
+dependencies = [2804
+ "phf_shared",2805
+ "serde",2806
+]2807
+2808
+[[package]]2809
+name = "phf_codegen"2810
+version = "0.13.1"2811
+source = "registry+https://github.com/rust-lang/crates.io-index"2812
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"2813
+dependencies = [2814
+ "phf_generator",2815
+ "phf_shared",2816
+]2817
+2818
+[[package]]2819
+name = "phf_generator"2820
+version = "0.13.1"2821
+source = "registry+https://github.com/rust-lang/crates.io-index"2822
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"2823
+dependencies = [2824
+ "fastrand",2825
+ "phf_shared",2826
+]2827
+2828
+[[package]]2829
+name = "phf_shared"2830
+version = "0.13.1"2831
+source = "registry+https://github.com/rust-lang/crates.io-index"2832
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"2833
+dependencies = [2834
+ "siphasher",2835
+]2836
+2837
+[[package]]2838
+name = "pin-project-lite"2839
+version = "0.2.17"2840
+source = "registry+https://github.com/rust-lang/crates.io-index"2841
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"2842
+2843
+[[package]]2844
+name = "pkcs8"2845
+version = "0.10.2"2846
+source = "registry+https://github.com/rust-lang/crates.io-index"2847
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"2848
+dependencies = [2849
+ "der",2850
+ "spki",2851
+]2852
+2853
+[[package]]2854
+name = "pkg-config"2855
+version = "0.3.34"2856
+source = "registry+https://github.com/rust-lang/crates.io-index"2857
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"2858
+2859
+[[package]]2860
+name = "poly1305"2861
+version = "0.8.0"2862
+source = "registry+https://github.com/rust-lang/crates.io-index"2863
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"2864
+dependencies = [2865
+ "cpufeatures 0.2.17",2866
+ "opaque-debug",2867
+ "universal-hash",2868
+]2869
+2870
+[[package]]2871
+name = "potential_utf"2872
+version = "0.1.6"2873
+source = "registry+https://github.com/rust-lang/crates.io-index"2874
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"2875
+dependencies = [2876
+ "zerovec",2877
+]2878
+2879
+[[package]]2880
+name = "powerfmt"2881
+version = "0.2.0"2882
+source = "registry+https://github.com/rust-lang/crates.io-index"2883
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"2884
+2885
+[[package]]2886
+name = "ppv-lite86"2887
+version = "0.2.21"2888
+source = "registry+https://github.com/rust-lang/crates.io-index"2889
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"2890
+dependencies = [2891
+ "zerocopy",2892
+]2893
+2894
+[[package]]2895
+name = "precomputed-hash"2896
+version = "0.1.1"2897
+source = "registry+https://github.com/rust-lang/crates.io-index"2898
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"2899
+2900
+[[package]]2901
+name = "proc-macro-crate"2902
+version = "3.5.0"2903
+source = "registry+https://github.com/rust-lang/crates.io-index"2904
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"2905
+dependencies = [2906
+ "toml_edit",2907
+]2908
+2909
+[[package]]2910
+name = "proc-macro-error-attr2"2911
+version = "2.0.0"2912
+source = "registry+https://github.com/rust-lang/crates.io-index"2913
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"2914
+dependencies = [2915
+ "proc-macro2",2916
+ "quote",2917
+]2918
+2919
+[[package]]2920
+name = "proc-macro-error2"2921
+version = "2.0.1"2922
+source = "registry+https://github.com/rust-lang/crates.io-index"2923
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"2924
+dependencies = [2925
+ "proc-macro-error-attr2",2926
+ "proc-macro2",2927
+ "quote",2928
+]2929
+2930
+[[package]]2931
+name = "proc-macro2"2932
+version = "1.0.107"2933
+source = "registry+https://github.com/rust-lang/crates.io-index"2934
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"2935
+dependencies = [2936
+ "unicode-ident",2937
+]2938
+2939
+[[package]]2940
+name = "prost"2941
+version = "0.14.4"2942
+source = "registry+https://github.com/rust-lang/crates.io-index"2943
+checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"2944
+dependencies = [2945
+ "bytes",2946
+ "prost-derive",2947
+]2948
+2949
+[[package]]2950
+name = "prost-derive"2951
+version = "0.14.4"2952
+source = "registry+https://github.com/rust-lang/crates.io-index"2953
+checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"2954
+dependencies = [2955
+ "anyhow",2956
+ "itertools 0.14.0",2957
+ "proc-macro2",2958
+ "quote",2959
+ "syn 2.0.119",2960
+]2961
+2962
+[[package]]2963
+name = "quinn"2964
+version = "0.11.12"2965
+source = "registry+https://github.com/rust-lang/crates.io-index"2966
+checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"2967
+dependencies = [2968
+ "bytes",2969
+ "cfg_aliases",2970
+ "pin-project-lite",2971
+ "quinn-proto",2972
+ "quinn-udp",2973
+ "rustc-hash",2974
+ "rustls",2975
+ "socket2",2976
+ "thiserror 2.0.21",2977
+ "tokio",2978
+ "tracing",2979
+ "web-time",2980
+]2981
+2982
+[[package]]2983
+name = "quinn-proto"2984
+version = "0.11.18"2985
+source = "registry+https://github.com/rust-lang/crates.io-index"2986
+checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"2987
+dependencies = [2988
+ "aws-lc-rs",2989
+ "bytes",2990
+ "getrandom 0.4.3",2991
+ "lru-slab",2992
+ "rand 0.10.3",2993
+ "rand_pcg",2994
+ "ring",2995
+ "rustc-hash",2996
+ "rustls",2997
+ "rustls-pki-types",2998
+ "slab",2999
+ "thiserror 2.0.21",3000
+ "tinyvec",3001
+ "tracing",3002
+ "web-time",3003
+]3004
+3005
+[[package]]3006
+name = "quinn-udp"3007
+version = "0.5.15"3008
+source = "registry+https://github.com/rust-lang/crates.io-index"3009
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"3010
+dependencies = [3011
+ "cfg_aliases",3012
+ "libc",3013
+ "once_cell",3014
+ "socket2",3015
+ "tracing",3016
+ "windows-sys 0.61.2",3017
+]3018
+3019
+[[package]]3020
+name = "quote"3021
+version = "1.0.47"3022
+source = "registry+https://github.com/rust-lang/crates.io-index"3023
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"3024
+dependencies = [3025
+ "proc-macro2",3026
+]3027
+3028
+[[package]]3029
+name = "r-efi"3030
+version = "5.3.0"3031
+source = "registry+https://github.com/rust-lang/crates.io-index"3032
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"3033
+3034
+[[package]]3035
+name = "r-efi"3036
+version = "6.0.0"3037
+source = "registry+https://github.com/rust-lang/crates.io-index"3038
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"3039
+3040
+[[package]]3041
+name = "rand"3042
+version = "0.8.8"3043
+source = "registry+https://github.com/rust-lang/crates.io-index"3044
+checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"3045
+dependencies = [3046
+ "libc",3047
+ "rand_chacha 0.3.1",3048
+ "rand_core 0.6.4",3049
+]3050
+3051
+[[package]]3052
+name = "rand"3053
+version = "0.9.5"3054
+source = "registry+https://github.com/rust-lang/crates.io-index"3055
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"3056
+dependencies = [3057
+ "rand_chacha 0.9.0",3058
+ "rand_core 0.9.5",3059
+]3060
+3061
+[[package]]3062
+name = "rand"3063
+version = "0.10.3"3064
+source = "registry+https://github.com/rust-lang/crates.io-index"3065
+checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"3066
+dependencies = [3067
+ "chacha20 0.10.2",3068
+ "getrandom 0.4.3",3069
+ "rand_core 0.10.1",3070
+]3071
+3072
+[[package]]3073
+name = "rand_chacha"3074
+version = "0.3.1"3075
+source = "registry+https://github.com/rust-lang/crates.io-index"3076
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"3077
+dependencies = [3078
+ "ppv-lite86",3079
+ "rand_core 0.6.4",3080
+]3081
+3082
+[[package]]3083
+name = "rand_chacha"3084
+version = "0.9.0"3085
+source = "registry+https://github.com/rust-lang/crates.io-index"3086
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"3087
+dependencies = [3088
+ "ppv-lite86",3089
+ "rand_core 0.9.5",3090
+]3091
+3092
+[[package]]3093
+name = "rand_core"3094
+version = "0.6.4"3095
+source = "registry+https://github.com/rust-lang/crates.io-index"3096
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"3097
+dependencies = [3098
+ "getrandom 0.2.17",3099
+]3100
+3101
+[[package]]3102
+name = "rand_core"3103
+version = "0.9.5"3104
+source = "registry+https://github.com/rust-lang/crates.io-index"3105
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"3106
+dependencies = [3107
+ "getrandom 0.3.4",3108
+]3109
+3110
+[[package]]3111
+name = "rand_core"3112
+version = "0.10.1"3113
+source = "registry+https://github.com/rust-lang/crates.io-index"3114
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"3115
+3116
+[[package]]3117
+name = "rand_pcg"3118
+version = "0.10.2"3119
+source = "registry+https://github.com/rust-lang/crates.io-index"3120
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"3121
+dependencies = [3122
+ "rand_core 0.10.1",3123
+]3124
+3125
+[[package]]3126
+name = "rand_xoshiro"3127
+version = "0.7.0"3128
+source = "registry+https://github.com/rust-lang/crates.io-index"3129
+checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"3130
+dependencies = [3131
+ "rand_core 0.9.5",3132
+]3133
+3134
+[[package]]3135
+name = "readlock"3136
+version = "0.1.11"3137
+source = "registry+https://github.com/rust-lang/crates.io-index"3138
+checksum = "6da6f291b23556edd9edaf655a0be2ad8ef8002ff5f1bca62b264f3f58b53f34"3139
+3140
+[[package]]3141
+name = "readlock-tokio"3142
+version = "0.1.6"3143
+source = "registry+https://github.com/rust-lang/crates.io-index"3144
+checksum = "fc7e264f9ec4f3d112e8e2f214e8e7cb5cf3b83278f3570b7e00bfe13d3bd8ff"3145
+dependencies = [3146
+ "tokio",3147
+]3148
+3149
+[[package]]3150
+name = "redox_syscall"3151
+version = "0.5.18"3152
+source = "registry+https://github.com/rust-lang/crates.io-index"3153
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"3154
+dependencies = [3155
+ "bitflags",3156
+]3157
+3158
+[[package]]3159
+name = "regex"3160
+version = "1.13.1"3161
+source = "registry+https://github.com/rust-lang/crates.io-index"3162
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"3163
+dependencies = [3164
+ "aho-corasick",3165
+ "memchr",3166
+ "regex-automata",3167
+ "regex-syntax",3168
+]3169
+3170
+[[package]]3171
+name = "regex-automata"3172
+version = "0.4.18"3173
+source = "registry+https://github.com/rust-lang/crates.io-index"3174
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"3175
+dependencies = [3176
+ "aho-corasick",3177
+ "memchr",3178
+ "regex-syntax",3179
+]3180
+3181
+[[package]]3182
+name = "regex-syntax"3183
+version = "0.8.11"3184
+source = "registry+https://github.com/rust-lang/crates.io-index"3185
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"3186
+3187
+[[package]]3188
+name = "reqwest"3189
+version = "0.13.5"3190
+source = "registry+https://github.com/rust-lang/crates.io-index"3191
+checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"3192
+dependencies = [3193
+ "base64 0.23.1",3194
+ "bytes",3195
+ "futures-core",3196
+ "futures-util",3197
+ "h2",3198
+ "http",3199
+ "http-body",3200
+ "http-body-util",3201
+ "hyper",3202
+ "hyper-rustls",3203
+ "hyper-util",3204
+ "js-sys",3205
+ "log",3206
+ "percent-encoding",3207
+ "pin-project-lite",3208
+ "quinn",3209
+ "rustls",3210
+ "rustls-pki-types",3211
+ "rustls-platform-verifier",3212
+ "serde",3213
+ "serde_json",3214
+ "serde_urlencoded",3215
+ "sync_wrapper",3216
+ "tokio",3217
+ "tokio-rustls",3218
+ "tokio-util",3219
+ "tower",3220
+ "tower-http",3221
+ "tower-service",3222
+ "url",3223
+ "wasm-bindgen",3224
+ "wasm-bindgen-futures",3225
+ "wasm-streams",3226
+ "web-sys",3227
+]3228
+3229
+[[package]]3230
+name = "ring"3231
+version = "0.17.14"3232
+source = "registry+https://github.com/rust-lang/crates.io-index"3233
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"3234
+dependencies = [3235
+ "cc",3236
+ "cfg-if",3237
+ "getrandom 0.2.17",3238
+ "libc",3239
+ "untrusted",3240
+ "windows-sys 0.52.0",3241
+]3242
+3243
+[[package]]3244
+name = "rmp"3245
+version = "0.8.15"3246
+source = "registry+https://github.com/rust-lang/crates.io-index"3247
+checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"3248
+dependencies = [3249
+ "num-traits",3250
+]3251
+3252
+[[package]]3253
+name = "rmp-serde"3254
+version = "1.3.1"3255
+source = "registry+https://github.com/rust-lang/crates.io-index"3256
+checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"3257
+dependencies = [3258
+ "rmp",3259
+ "serde",3260
+]3261
+3262
+[[package]]3263
+name = "ruma"3264
+version = "0.16.0"3265
+source = "registry+https://github.com/rust-lang/crates.io-index"3266
+checksum = "ee4fe5bfacdb0e95e733da3b6c37d98edf46447a4a8e8dea824e0da266d8ad59"3267
+dependencies = [3268
+ "assign",3269
+ "js_int",3270
+ "js_option",3271
+ "ruma-client-api",3272
+ "ruma-common",3273
+ "ruma-events",3274
+ "ruma-html",3275
+ "web-time",3276
+]3277
+3278
+[[package]]3279
+name = "ruma-client-api"3280
+version = "0.24.0"3281
+source = "registry+https://github.com/rust-lang/crates.io-index"3282
+checksum = "bf7ca43a888ca569168d7e3901f4dd14a777b860bb19f4c08e35414162eb261c"3283
+dependencies = [3284
+ "as_variant",3285
+ "assign",3286
+ "bytes",3287
+ "http",3288
+ "js_int",3289
+ "js_option",3290
+ "maplit",3291
+ "ruma-common",3292
+ "ruma-events",3293
+ "serde",3294
+ "serde_html_form",3295
+ "serde_json",3296
+ "thiserror 2.0.21",3297
+ "url",3298
+ "web-time",3299
+]3300
+3301
+[[package]]3302
+name = "ruma-common"3303
+version = "0.19.0"3304
+source = "registry+https://github.com/rust-lang/crates.io-index"3305
+checksum = "2c3b4f00112791b490acce57df1ce3eb3f88899b045bebcff8a29f75369640cc"3306
+dependencies = [3307
+ "as_variant",3308
+ "base64 0.22.1",3309
+ "bytes",3310
+ "date_header",3311
+ "form_urlencoded",3312
+ "getrandom 0.4.3",3313
+ "http",3314
+ "indexmap",3315
+ "js_int",3316
+ "konst",3317
+ "percent-encoding",3318
+ "rand 0.10.3",3319
+ "regex",3320
+ "ruma-identifiers-validation",3321
+ "ruma-macros",3322
+ "serde",3323
+ "serde_html_form",3324
+ "serde_json",3325
+ "thiserror 2.0.21",3326
+ "time",3327
+ "tracing",3328
+ "url",3329
+ "uuid",3330
+ "web-time",3331
+ "wildmatch",3332
+ "zeroize",3333
+]3334
+3335
+[[package]]3336
+name = "ruma-events"3337
+version = "0.34.0"3338
+source = "registry+https://github.com/rust-lang/crates.io-index"3339
+checksum = "85d2f90830fc131691349b96a69ff53444eb6c3e8dc7869c77961b43cfaf3344"3340
+dependencies = [3341
+ "as_variant",3342
+ "indexmap",3343
+ "js_int",3344
+ "js_option",3345
+ "ruma-common",3346
+ "ruma-html",3347
+ "ruma-macros",3348
+ "serde",3349
+ "serde_json",3350
+ "thiserror 2.0.21",3351
+ "tracing",3352
+ "web-time",3353
+ "wildmatch",3354
+ "zeroize",3355
+]3356
+3357
+[[package]]3358
+name = "ruma-html"3359
+version = "0.8.0"3360
+source = "registry+https://github.com/rust-lang/crates.io-index"3361
+checksum = "48d33a944650f4bbd2188dd204d39dd87a9a1498f14b3a13252910c90ed7cd43"3362
+dependencies = [3363
+ "as_variant",3364
+ "html5ever",3365
+ "tracing",3366
+ "wildmatch",3367
+]3368
+3369
+[[package]]3370
+name = "ruma-identifiers-validation"3371
+version = "0.12.1"3372
+source = "registry+https://github.com/rust-lang/crates.io-index"3373
+checksum = "9d6cff00317675f487c4e7ccfb18875a14c5a14867b51d13f2a826053f03c432"3374
+dependencies = [3375
+ "js_int",3376
+ "thiserror 2.0.21",3377
+]3378
+3379
+[[package]]3380
+name = "ruma-macros"3381
+version = "0.19.0"3382
+source = "registry+https://github.com/rust-lang/crates.io-index"3383
+checksum = "c8cfb39eaa9b9fd389126ff941e060b496add5cbbfef559d80c46e571dda459c"3384
+dependencies = [3385
+ "as_variant",3386
+ "cfg-if",3387
+ "proc-macro-crate",3388
+ "proc-macro2",3389
+ "quote",3390
+ "ruma-identifiers-validation",3391
+ "serde",3392
+ "syn 2.0.119",3393
+ "toml",3394
+]3395
+3396
+[[package]]3397
+name = "rusqlite"3398
+version = "0.37.0"3399
+source = "registry+https://github.com/rust-lang/crates.io-index"3400
+checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"3401
+dependencies = [3402
+ "bitflags",3403
+ "fallible-iterator",3404
+ "fallible-streaming-iterator",3405
+ "hashlink",3406
+ "libsqlite3-sys",3407
+ "smallvec",3408
+]3409
+3410
+[[package]]3411
+name = "rustc-hash"3412
+version = "2.1.3"3413
+source = "registry+https://github.com/rust-lang/crates.io-index"3414
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"3415
+3416
+[[package]]3417
+name = "rustc_version"3418
+version = "0.4.1"3419
+source = "registry+https://github.com/rust-lang/crates.io-index"3420
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"3421
+dependencies = [3422
+ "semver",3423
+]3424
+3425
+[[package]]3426
+name = "rustix"3427
+version = "1.1.5"3428
+source = "registry+https://github.com/rust-lang/crates.io-index"3429
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"3430
+dependencies = [3431
+ "bitflags",3432
+ "errno",3433
+ "libc",3434
+ "linux-raw-sys",3435
+ "windows-sys 0.61.2",3436
+]3437
+3438
+[[package]]3439
+name = "rustls"3440
+version = "0.23.45"3441
+source = "registry+https://github.com/rust-lang/crates.io-index"3442
+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"3443
+dependencies = [3444
+ "aws-lc-rs",3445
+ "log",3446
+ "once_cell",3447
+ "rustls-pki-types",3448
+ "rustls-webpki",3449
+ "subtle",3450
+ "zeroize",3451
+]3452
+3453
+[[package]]3454
+name = "rustls-native-certs"3455
+version = "0.8.4"3456
+source = "registry+https://github.com/rust-lang/crates.io-index"3457
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"3458
+dependencies = [3459
+ "openssl-probe",3460
+ "rustls-pki-types",3461
+ "schannel",3462
+ "security-framework",3463
+]3464
+3465
+[[package]]3466
+name = "rustls-pki-types"3467
+version = "1.15.1"3468
+source = "registry+https://github.com/rust-lang/crates.io-index"3469
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"3470
+dependencies = [3471
+ "web-time",3472
+ "zeroize",3473
+]3474
+3475
+[[package]]3476
+name = "rustls-platform-verifier"3477
+version = "0.7.0"3478
+source = "registry+https://github.com/rust-lang/crates.io-index"3479
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"3480
+dependencies = [3481
+ "core-foundation",3482
+ "core-foundation-sys",3483
+ "jni",3484
+ "log",3485
+ "once_cell",3486
+ "rustls",3487
+ "rustls-native-certs",3488
+ "rustls-platform-verifier-android",3489
+ "rustls-webpki",3490
+ "security-framework",3491
+ "security-framework-sys",3492
+ "webpki-root-certs",3493
+ "windows-sys 0.61.2",3494
+]3495
+3496
+[[package]]3497
+name = "rustls-platform-verifier-android"3498
+version = "0.1.1"3499
+source = "registry+https://github.com/rust-lang/crates.io-index"3500
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"3501
+3502
+[[package]]3503
+name = "rustls-webpki"3504
+version = "0.103.15"3505
+source = "registry+https://github.com/rust-lang/crates.io-index"3506
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"3507
+dependencies = [3508
+ "aws-lc-rs",3509
+ "ring",3510
+ "rustls-pki-types",3511
+ "untrusted",3512
+]3513
+3514
+[[package]]3515
+name = "rustversion"3516
+version = "1.0.23"3517
+source = "registry+https://github.com/rust-lang/crates.io-index"3518
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"3519
+3520
+[[package]]3521
+name = "ryu"3522
+version = "1.0.23"3523
+source = "registry+https://github.com/rust-lang/crates.io-index"3524
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"3525
+3526
+[[package]]3527
+name = "same-file"3528
+version = "1.0.6"3529
+source = "registry+https://github.com/rust-lang/crates.io-index"3530
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"3531
+dependencies = [3532
+ "winapi-util",3533
+]3534
+3535
+[[package]]3536
+name = "schannel"3537
+version = "0.1.29"3538
+source = "registry+https://github.com/rust-lang/crates.io-index"3539
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"3540
+dependencies = [3541
+ "windows-sys 0.61.2",3542
+]3543
+3544
+[[package]]3545
+name = "scoped-tls"3546
+version = "1.0.1"3547
+source = "registry+https://github.com/rust-lang/crates.io-index"3548
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"3549
+3550
+[[package]]3551
+name = "scopeguard"3552
+version = "1.2.0"3553
+source = "registry+https://github.com/rust-lang/crates.io-index"3554
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"3555
+3556
+[[package]]3557
+name = "sealed"3558
+version = "0.6.0"3559
+source = "registry+https://github.com/rust-lang/crates.io-index"3560
+checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"3561
+dependencies = [3562
+ "proc-macro2",3563
+ "quote",3564
+ "syn 2.0.119",3565
+]3566
+3567
+[[package]]3568
+name = "sealed"3569
+version = "0.7.0"3570
+source = "registry+https://github.com/rust-lang/crates.io-index"3571
+checksum = "9b68e2ea526d9fb32f23ca8894fb5da9e743f34c2f41701f0501dc8a25c4b343"3572
+dependencies = [3573
+ "proc-macro2",3574
+ "quote",3575
+ "syn 3.0.6",3576
+]3577
+3578
+[[package]]3579
+name = "security-framework"3580
+version = "3.7.0"3581
+source = "registry+https://github.com/rust-lang/crates.io-index"3582
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"3583
+dependencies = [3584
+ "bitflags",3585
+ "core-foundation",3586
+ "core-foundation-sys",3587
+ "libc",3588
+ "security-framework-sys",3589
+]3590
+3591
+[[package]]3592
+name = "security-framework-sys"3593
+version = "2.17.0"3594
+source = "registry+https://github.com/rust-lang/crates.io-index"3595
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"3596
+dependencies = [3597
+ "core-foundation-sys",3598
+ "libc",3599
+]3600
+3601
+[[package]]3602
+name = "semver"3603
+version = "1.0.28"3604
+source = "registry+https://github.com/rust-lang/crates.io-index"3605
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"3606
+3607
+[[package]]3608
+name = "serde"3609
+version = "1.0.229"3610
+source = "registry+https://github.com/rust-lang/crates.io-index"3611
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"3612
+dependencies = [3613
+ "serde_core",3614
+ "serde_derive",3615
+]3616
+3617
+[[package]]3618
+name = "serde-wasm-bindgen"3619
+version = "0.6.5"3620
+source = "registry+https://github.com/rust-lang/crates.io-index"3621
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"3622
+dependencies = [3623
+ "js-sys",3624
+ "serde",3625
+ "wasm-bindgen",3626
+]3627
+3628
+[[package]]3629
+name = "serde_bytes"3630
+version = "0.11.19"3631
+source = "registry+https://github.com/rust-lang/crates.io-index"3632
+checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"3633
+dependencies = [3634
+ "serde",3635
+ "serde_core",3636
+]3637
+3638
+[[package]]3639
+name = "serde_core"3640
+version = "1.0.229"3641
+source = "registry+https://github.com/rust-lang/crates.io-index"3642
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"3643
+dependencies = [3644
+ "serde_derive",3645
+]3646
+3647
+[[package]]3648
+name = "serde_derive"3649
+version = "1.0.229"3650
+source = "registry+https://github.com/rust-lang/crates.io-index"3651
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"3652
+dependencies = [3653
+ "proc-macro2",3654
+ "quote",3655
+ "syn 3.0.6",3656
+]3657
+3658
+[[package]]3659
+name = "serde_html_form"3660
+version = "0.4.1"3661
+source = "registry+https://github.com/rust-lang/crates.io-index"3662
+checksum = "8f0346d7a342ab90f405cfc08f25d15075f944f42fcabbc5eac923829fa6d228"3663
+dependencies = [3664
+ "form_urlencoded",3665
+ "indexmap",3666
+ "itoa",3667
+ "serde_core",3668
+ "zmij",3669
+]3670
+3671
+[[package]]3672
+name = "serde_json"3673
+version = "1.0.151"3674
+source = "registry+https://github.com/rust-lang/crates.io-index"3675
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"3676
+dependencies = [3677
+ "itoa",3678
+ "memchr",3679
+ "serde",3680
+ "serde_core",3681
+ "zmij",3682
+]3683
+3684
+[[package]]3685
+name = "serde_path_to_error"3686
+version = "0.1.20"3687
+source = "registry+https://github.com/rust-lang/crates.io-index"3688
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"3689
+dependencies = [3690
+ "itoa",3691
+ "serde",3692
+ "serde_core",3693
+]3694
+3695
+[[package]]3696
+name = "serde_spanned"3697
+version = "1.1.1"3698
+source = "registry+https://github.com/rust-lang/crates.io-index"3699
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"3700
+dependencies = [3701
+ "serde_core",3702
+]3703
+3704
+[[package]]3705
+name = "serde_urlencoded"3706
+version = "0.7.1"3707
+source = "registry+https://github.com/rust-lang/crates.io-index"3708
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"3709
+dependencies = [3710
+ "form_urlencoded",3711
+ "itoa",3712
+ "ryu",3713
+ "serde",3714
+]3715
+3716
+[[package]]3717
+name = "sha2"3718
+version = "0.10.9"3719
+source = "registry+https://github.com/rust-lang/crates.io-index"3720
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"3721
+dependencies = [3722
+ "cfg-if",3723
+ "cpufeatures 0.2.17",3724
+ "digest",3725
+]3726
+3727
+[[package]]3728
+name = "sharded-slab"3729
+version = "0.1.7"3730
+source = "registry+https://github.com/rust-lang/crates.io-index"3731
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"3732
+dependencies = [3733
+ "lazy_static",3734
+]3735
+3736
+[[package]]3737
+name = "shlex"3738
+version = "2.0.1"3739
+source = "registry+https://github.com/rust-lang/crates.io-index"3740
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"3741
+3742
+[[package]]3743
+name = "signal-hook-registry"3744
+version = "1.4.8"3745
+source = "registry+https://github.com/rust-lang/crates.io-index"3746
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"3747
+dependencies = [3748
+ "errno",3749
+ "libc",3750
+]3751
+3752
+[[package]]3753
+name = "signature"3754
+version = "2.2.0"3755
+source = "registry+https://github.com/rust-lang/crates.io-index"3756
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"3757
+dependencies = [3758
+ "rand_core 0.6.4",3759
+]3760
+3761
+[[package]]3762
+name = "simd-adler32"3763
+version = "0.3.10"3764
+source = "registry+https://github.com/rust-lang/crates.io-index"3765
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"3766
+3767
+[[package]]3768
+name = "simd_cesu8"3769
+version = "1.2.0"3770
+source = "registry+https://github.com/rust-lang/crates.io-index"3771
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"3772
+dependencies = [3773
+ "rustc_version",3774
+ "simdutf8",3775
+]3776
+3777
+[[package]]3778
+name = "simdutf8"3779
+version = "0.1.5"3780
+source = "registry+https://github.com/rust-lang/crates.io-index"3781
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"3782
+3783
+[[package]]3784
+name = "similar"3785
+version = "2.7.0"3786
+source = "registry+https://github.com/rust-lang/crates.io-index"3787
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"3788
+3789
+[[package]]3790
+name = "siphasher"3791
+version = "1.0.3"3792
+source = "registry+https://github.com/rust-lang/crates.io-index"3793
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"3794
+3795
+[[package]]3796
+name = "slab"3797
+version = "0.4.12"3798
+source = "registry+https://github.com/rust-lang/crates.io-index"3799
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"3800
+3801
+[[package]]3802
+name = "smallvec"3803
+version = "1.16.1"3804
+source = "registry+https://github.com/rust-lang/crates.io-index"3805
+checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"3806
+dependencies = [3807
+ "serde",3808
+]3809
+3810
+[[package]]3811
+name = "socket2"3812
+version = "0.6.5"3813
+source = "registry+https://github.com/rust-lang/crates.io-index"3814
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"3815
+dependencies = [3816
+ "libc",3817
+ "windows-sys 0.61.2",3818
+]3819
+3820
+[[package]]3821
+name = "spin"3822
+version = "0.9.9"3823
+source = "registry+https://github.com/rust-lang/crates.io-index"3824
+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"3825
+3826
+[[package]]3827
+name = "spki"3828
+version = "0.7.3"3829
+source = "registry+https://github.com/rust-lang/crates.io-index"3830
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"3831
+dependencies = [3832
+ "base64ct",3833
+ "der",3834
+]3835
+3836
+[[package]]3837
+name = "stable_deref_trait"3838
+version = "1.2.1"3839
+source = "registry+https://github.com/rust-lang/crates.io-index"3840
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"3841
+3842
+[[package]]3843
+name = "string_cache"3844
+version = "0.9.0"3845
+source = "registry+https://github.com/rust-lang/crates.io-index"3846
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"3847
+dependencies = [3848
+ "new_debug_unreachable",3849
+ "parking_lot",3850
+ "phf_shared",3851
+ "precomputed-hash",3852
+]3853
+3854
+[[package]]3855
+name = "string_cache_codegen"3856
+version = "0.6.1"3857
+source = "registry+https://github.com/rust-lang/crates.io-index"3858
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"3859
+dependencies = [3860
+ "phf_generator",3861
+ "phf_shared",3862
+ "proc-macro2",3863
+ "quote",3864
+]3865
+3866
+[[package]]3867
+name = "subtle"3868
+version = "2.6.1"3869
+source = "registry+https://github.com/rust-lang/crates.io-index"3870
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"3871
+3872
+[[package]]3873
+name = "syn"3874
+version = "1.0.109"3875
+source = "registry+https://github.com/rust-lang/crates.io-index"3876
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"3877
+dependencies = [3878
+ "proc-macro2",3879
+ "quote",3880
+ "unicode-ident",3881
+]3882
+3883
+[[package]]3884
+name = "syn"3885
+version = "2.0.119"3886
+source = "registry+https://github.com/rust-lang/crates.io-index"3887
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"3888
+dependencies = [3889
+ "proc-macro2",3890
+ "quote",3891
+ "unicode-ident",3892
+]3893
+3894
+[[package]]3895
+name = "syn"3896
+version = "3.0.6"3897
+source = "registry+https://github.com/rust-lang/crates.io-index"3898
+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"3899
+dependencies = [3900
+ "proc-macro2",3901
+ "quote",3902
+ "unicode-ident",3903
+]3904
+3905
+[[package]]3906
+name = "sync_wrapper"3907
+version = "1.0.2"3908
+source = "registry+https://github.com/rust-lang/crates.io-index"3909
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"3910
+dependencies = [3911
+ "futures-core",3912
+]3913
+3914
+[[package]]3915
+name = "synstructure"3916
+version = "0.14.0"3917
+source = "registry+https://github.com/rust-lang/crates.io-index"3918
+checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"3919
+dependencies = [3920
+ "proc-macro2",3921
+ "quote",3922
+ "syn 3.0.6",3923
+]3924
+3925
+[[package]]3926
+name = "tempfile"3927
+version = "3.27.0"3928
+source = "registry+https://github.com/rust-lang/crates.io-index"3929
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"3930
+dependencies = [3931
+ "fastrand",3932
+ "getrandom 0.4.3",3933
+ "once_cell",3934
+ "rustix",3935
+ "windows-sys 0.61.2",3936
+]3937
+3938
+[[package]]3939
+name = "tendril"3940
+version = "0.5.1"3941
+source = "registry+https://github.com/rust-lang/crates.io-index"3942
+checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"3943
+dependencies = [3944
+ "new_debug_unreachable",3945
+]3946
+3947
+[[package]]3948
+name = "thiserror"3949
+version = "1.0.69"3950
+source = "registry+https://github.com/rust-lang/crates.io-index"3951
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"3952
+dependencies = [3953
+ "thiserror-impl 1.0.69",3954
+]3955
+3956
+[[package]]3957
+name = "thiserror"3958
+version = "2.0.21"3959
+source = "registry+https://github.com/rust-lang/crates.io-index"3960
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"3961
+dependencies = [3962
+ "thiserror-impl 2.0.21",3963
+]3964
+3965
+[[package]]3966
+name = "thiserror-impl"3967
+version = "1.0.69"3968
+source = "registry+https://github.com/rust-lang/crates.io-index"3969
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"3970
+dependencies = [3971
+ "proc-macro2",3972
+ "quote",3973
+ "syn 2.0.119",3974
+]3975
+3976
+[[package]]3977
+name = "thiserror-impl"3978
+version = "2.0.21"3979
+source = "registry+https://github.com/rust-lang/crates.io-index"3980
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"3981
+dependencies = [3982
+ "proc-macro2",3983
+ "quote",3984
+ "syn 3.0.6",3985
+]3986
+3987
+[[package]]3988
+name = "thread_local"3989
+version = "1.1.10"3990
+source = "registry+https://github.com/rust-lang/crates.io-index"3991
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"3992
+dependencies = [3993
+ "cfg-if",3994
+]3995
+3996
+[[package]]3997
+name = "time"3998
+version = "0.3.55"3999
+source = "registry+https://github.com/rust-lang/crates.io-index"4000
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"4001
+dependencies = [4002
+ "deranged",4003
+ "num-conv",4004
+ "powerfmt",4005
+ "serde_core",4006
+ "time-core",4007
+ "time-macros",4008
+]4009
+4010
+[[package]]4011
+name = "time-core"4012
+version = "0.1.9"4013
+source = "registry+https://github.com/rust-lang/crates.io-index"4014
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"4015
+4016
+[[package]]4017
+name = "time-macros"4018
+version = "0.2.32"4019
+source = "registry+https://github.com/rust-lang/crates.io-index"4020
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"4021
+dependencies = [4022
+ "num-conv",4023
+ "time-core",4024
+]4025
+4026
+[[package]]4027
+name = "tinystr"4028
+version = "0.8.4"4029
+source = "registry+https://github.com/rust-lang/crates.io-index"4030
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"4031
+dependencies = [4032
+ "displaydoc",4033
+ "zerovec",4034
+]4035
+4036
+[[package]]4037
+name = "tinyvec"4038
+version = "1.13.3"4039
+source = "registry+https://github.com/rust-lang/crates.io-index"4040
+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"4041
+4042
+[[package]]4043
+name = "tokio"4044
+version = "1.53.1"4045
+source = "registry+https://github.com/rust-lang/crates.io-index"4046
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"4047
+dependencies = [4048
+ "bytes",4049
+ "libc",4050
+ "mio",4051
+ "parking_lot",4052
+ "pin-project-lite",4053
+ "signal-hook-registry",4054
+ "socket2",4055
+ "tokio-macros",4056
+ "windows-sys 0.61.2",4057
+]4058
+4059
+[[package]]4060
+name = "tokio-macros"4061
+version = "2.7.2"4062
+source = "registry+https://github.com/rust-lang/crates.io-index"4063
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"4064
+dependencies = [4065
+ "proc-macro2",4066
+ "quote",4067
+ "syn 3.0.6",4068
+]4069
+4070
+[[package]]4071
+name = "tokio-rustls"4072
+version = "0.26.5"4073
+source = "registry+https://github.com/rust-lang/crates.io-index"4074
+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"4075
+dependencies = [4076
+ "rustls",4077
+ "tokio",4078
+]4079
+4080
+[[package]]4081
+name = "tokio-stream"4082
+version = "0.1.19"4083
+source = "registry+https://github.com/rust-lang/crates.io-index"4084
+checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"4085
+dependencies = [4086
+ "futures-core",4087
+ "pin-project-lite",4088
+ "tokio",4089
+ "tokio-util",4090
+]4091
+4092
+[[package]]4093
+name = "tokio-util"4094
+version = "0.7.19"4095
+source = "registry+https://github.com/rust-lang/crates.io-index"4096
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"4097
+dependencies = [4098
+ "bytes",4099
+ "futures-core",4100
+ "futures-sink",4101
+ "libc",4102
+ "pin-project-lite",4103
+ "tokio",4104
+]4105
+4106
+[[package]]4107
+name = "toml"4108
+version = "1.1.6+spec-1.1.0"4109
+source = "registry+https://github.com/rust-lang/crates.io-index"4110
+checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"4111
+dependencies = [4112
+ "serde_core",4113
+ "serde_spanned",4114
+ "toml_datetime",4115
+ "toml_parser",4116
+ "winnow",4117
+]4118
+4119
+[[package]]4120
+name = "toml_datetime"4121
+version = "1.1.1+spec-1.1.0"4122
+source = "registry+https://github.com/rust-lang/crates.io-index"4123
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"4124
+dependencies = [4125
+ "serde_core",4126
+]4127
+4128
+[[package]]4129
+name = "toml_edit"4130
+version = "0.25.15+spec-1.1.0"4131
+source = "registry+https://github.com/rust-lang/crates.io-index"4132
+checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"4133
+dependencies = [4134
+ "indexmap",4135
+ "toml_datetime",4136
+ "toml_parser",4137
+ "winnow",4138
+]4139
+4140
+[[package]]4141
+name = "toml_parser"4142
+version = "1.1.3+spec-1.1.0"4143
+source = "registry+https://github.com/rust-lang/crates.io-index"4144
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"4145
+dependencies = [4146
+ "winnow",4147
+]4148
+4149
+[[package]]4150
+name = "tower"4151
+version = "0.5.3"4152
+source = "registry+https://github.com/rust-lang/crates.io-index"4153
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"4154
+dependencies = [4155
+ "futures-core",4156
+ "futures-util",4157
+ "pin-project-lite",4158
+ "sync_wrapper",4159
+ "tokio",4160
+ "tower-layer",4161
+ "tower-service",4162
+ "tracing",4163
+]4164
+4165
+[[package]]4166
+name = "tower-http"4167
+version = "0.6.11"4168
+source = "registry+https://github.com/rust-lang/crates.io-index"4169
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"4170
+dependencies = [4171
+ "async-compression",4172
+ "bitflags",4173
+ "bytes",4174
+ "futures-core",4175
+ "futures-util",4176
+ "http",4177
+ "http-body",4178
+ "http-body-util",4179
+ "http-range-header",4180
+ "httpdate",4181
+ "mime",4182
+ "mime_guess",4183
+ "percent-encoding",4184
+ "pin-project-lite",4185
+ "tokio",4186
+ "tokio-util",4187
+ "tower",4188
+ "tower-layer",4189
+ "tower-service",4190
+ "tracing",4191
+ "url",4192
+]4193
+4194
+[[package]]4195
+name = "tower-layer"4196
+version = "0.3.3"4197
+source = "registry+https://github.com/rust-lang/crates.io-index"4198
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"4199
+4200
+[[package]]4201
+name = "tower-service"4202
+version = "0.3.3"4203
+source = "registry+https://github.com/rust-lang/crates.io-index"4204
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"4205
+4206
+[[package]]4207
+name = "tracing"4208
+version = "0.1.44"4209
+source = "registry+https://github.com/rust-lang/crates.io-index"4210
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"4211
+dependencies = [4212
+ "log",4213
+ "pin-project-lite",4214
+ "tracing-attributes",4215
+ "tracing-core",4216
+]4217
+4218
+[[package]]4219
+name = "tracing-attributes"4220
+version = "0.1.31"4221
+source = "registry+https://github.com/rust-lang/crates.io-index"4222
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"4223
+dependencies = [4224
+ "proc-macro2",4225
+ "quote",4226
+ "syn 2.0.119",4227
+]4228
+4229
+[[package]]4230
+name = "tracing-core"4231
+version = "0.1.36"4232
+source = "registry+https://github.com/rust-lang/crates.io-index"4233
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"4234
+dependencies = [4235
+ "once_cell",4236
+ "valuable",4237
+]4238
+4239
+[[package]]4240
+name = "tracing-log"4241
+version = "0.2.0"4242
+source = "registry+https://github.com/rust-lang/crates.io-index"4243
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"4244
+dependencies = [4245
+ "log",4246
+ "once_cell",4247
+ "tracing-core",4248
+]4249
+4250
+[[package]]4251
+name = "tracing-subscriber"4252
+version = "0.3.23"4253
+source = "registry+https://github.com/rust-lang/crates.io-index"4254
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"4255
+dependencies = [4256
+ "matchers",4257
+ "nu-ansi-term",4258
+ "once_cell",4259
+ "regex-automata",4260
+ "sharded-slab",4261
+ "smallvec",4262
+ "thread_local",4263
+ "tracing",4264
+ "tracing-core",4265
+ "tracing-log",4266
+]4267
+4268
+[[package]]4269
+name = "try-lock"4270
+version = "0.2.5"4271
+source = "registry+https://github.com/rust-lang/crates.io-index"4272
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"4273
+4274
+[[package]]4275
+name = "typenum"4276
+version = "1.20.1"4277
+source = "registry+https://github.com/rust-lang/crates.io-index"4278
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"4279
+4280
+[[package]]4281
+name = "typewit"4282
+version = "1.15.2"4283
+source = "registry+https://github.com/rust-lang/crates.io-index"4284
+checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"4285
+4286
+[[package]]4287
+name = "ucd-trie"4288
+version = "0.1.7"4289
+source = "registry+https://github.com/rust-lang/crates.io-index"4290
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"4291
+4292
+[[package]]4293
+name = "ulid"4294
+version = "1.2.1"4295
+source = "registry+https://github.com/rust-lang/crates.io-index"4296
+checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"4297
+dependencies = [4298
+ "rand 0.9.5",4299
+ "web-time",4300
+]4301
+4302
+[[package]]4303
+name = "unicase"4304
+version = "2.9.0"4305
+source = "registry+https://github.com/rust-lang/crates.io-index"4306
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"4307
+4308
+[[package]]4309
+name = "unicode-ident"4310
+version = "1.0.26"4311
+source = "registry+https://github.com/rust-lang/crates.io-index"4312
+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"4313
+4314
+[[package]]4315
+name = "unicode-normalization"4316
+version = "0.1.25"4317
+source = "registry+https://github.com/rust-lang/crates.io-index"4318
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"4319
+dependencies = [4320
+ "tinyvec",4321
+]4322
+4323
+[[package]]4324
+name = "unicode-segmentation"4325
+version = "1.13.3"4326
+source = "registry+https://github.com/rust-lang/crates.io-index"4327
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"4328
+4329
+[[package]]4330
+name = "unicode-xid"4331
+version = "0.2.6"4332
+source = "registry+https://github.com/rust-lang/crates.io-index"4333
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"4334
+4335
+[[package]]4336
+name = "universal-hash"4337
+version = "0.5.1"4338
+source = "registry+https://github.com/rust-lang/crates.io-index"4339
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"4340
+dependencies = [4341
+ "crypto-common",4342
+ "subtle",4343
+]4344
+4345
+[[package]]4346
+name = "untrusted"4347
+version = "0.9.0"4348
+source = "registry+https://github.com/rust-lang/crates.io-index"4349
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"4350
+4351
+[[package]]4352
+name = "url"4353
+version = "2.5.8"4354
+source = "registry+https://github.com/rust-lang/crates.io-index"4355
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"4356
+dependencies = [4357
+ "form_urlencoded",4358
+ "idna",4359
+ "percent-encoding",4360
+ "serde",4361
+ "serde_derive",4362
+]4363
+4364
+[[package]]4365
+name = "urlencoding"4366
+version = "2.1.3"4367
+source = "registry+https://github.com/rust-lang/crates.io-index"4368
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"4369
+4370
+[[package]]4371
+name = "utf8_iter"4372
+version = "1.0.4"4373
+source = "registry+https://github.com/rust-lang/crates.io-index"4374
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"4375
+4376
+[[package]]4377
+name = "uuid"4378
+version = "1.26.1"4379
+source = "registry+https://github.com/rust-lang/crates.io-index"4380
+checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"4381
+dependencies = [4382
+ "getrandom 0.4.3",4383
+ "js-sys",4384
+ "serde_core",4385
+ "wasm-bindgen",4386
+]4387
+4388
+[[package]]4389
+name = "valuable"4390
+version = "0.1.1"4391
+source = "registry+https://github.com/rust-lang/crates.io-index"4392
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"4393
+4394
+[[package]]4395
+name = "vcpkg"4396
+version = "0.2.15"4397
+source = "registry+https://github.com/rust-lang/crates.io-index"4398
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"4399
+4400
+[[package]]4401
+name = "version_check"4402
+version = "0.9.5"4403
+source = "registry+https://github.com/rust-lang/crates.io-index"4404
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"4405
+4406
+[[package]]4407
+name = "vodozemac"4408
+version = "0.10.0"4409
+source = "registry+https://github.com/rust-lang/crates.io-index"4410
+checksum = "b98bf83c0992966775b8012f194b07b44928996163e5a05b741b43891571ae5b"4411
+dependencies = [4412
+ "aes",4413
+ "arrayvec",4414
+ "base64 0.22.1",4415
+ "base64ct",4416
+ "cbc",4417
+ "chacha20poly1305",4418
+ "curve25519-dalek",4419
+ "ed25519-dalek",4420
+ "getrandom 0.2.17",4421
+ "hkdf",4422
+ "hmac",4423
+ "matrix-pickle",4424
+ "prost",4425
+ "rand 0.8.8",4426
+ "serde",4427
+ "serde_bytes",4428
+ "serde_json",4429
+ "sha2",4430
+ "subtle",4431
+ "thiserror 2.0.21",4432
+ "x25519-dalek",4433
+ "zeroize",4434
+]4435
+4436
+[[package]]4437
+name = "walkdir"4438
+version = "2.5.0"4439
+source = "registry+https://github.com/rust-lang/crates.io-index"4440
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"4441
+dependencies = [4442
+ "same-file",4443
+ "winapi-util",4444
+]4445
+4446
+[[package]]4447
+name = "want"4448
+version = "0.3.1"4449
+source = "registry+https://github.com/rust-lang/crates.io-index"4450
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"4451
+dependencies = [4452
+ "try-lock",4453
+]4454
+4455
+[[package]]4456
+name = "wasi"4457
+version = "0.11.1+wasi-snapshot-preview1"4458
+source = "registry+https://github.com/rust-lang/crates.io-index"4459
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"4460
+4461
+[[package]]4462
+name = "wasip2"4463
+version = "1.0.4+wasi-0.2.12"4464
+source = "registry+https://github.com/rust-lang/crates.io-index"4465
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"4466
+dependencies = [4467
+ "wit-bindgen",4468
+]4469
+4470
+[[package]]4471
+name = "wasm-bindgen"4472
+version = "0.2.128"4473
+source = "registry+https://github.com/rust-lang/crates.io-index"4474
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"4475
+dependencies = [4476
+ "cfg-if",4477
+ "once_cell",4478
+ "rustversion",4479
+ "wasm-bindgen-macro",4480
+ "wasm-bindgen-shared",4481
+]4482
+4483
+[[package]]4484
+name = "wasm-bindgen-futures"4485
+version = "0.4.78"4486
+source = "registry+https://github.com/rust-lang/crates.io-index"4487
+checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"4488
+dependencies = [4489
+ "js-sys",4490
+ "wasm-bindgen",4491
+]4492
+4493
+[[package]]4494
+name = "wasm-bindgen-macro"4495
+version = "0.2.128"4496
+source = "registry+https://github.com/rust-lang/crates.io-index"4497
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"4498
+dependencies = [4499
+ "quote",4500
+ "wasm-bindgen-macro-support",4501
+]4502
+4503
+[[package]]4504
+name = "wasm-bindgen-macro-support"4505
+version = "0.2.128"4506
+source = "registry+https://github.com/rust-lang/crates.io-index"4507
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"4508
+dependencies = [4509
+ "bumpalo",4510
+ "proc-macro2",4511
+ "quote",4512
+ "syn 3.0.6",4513
+ "wasm-bindgen-shared",4514
+]4515
+4516
+[[package]]4517
+name = "wasm-bindgen-shared"4518
+version = "0.2.128"4519
+source = "registry+https://github.com/rust-lang/crates.io-index"4520
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"4521
+dependencies = [4522
+ "unicode-ident",4523
+]4524
+4525
+[[package]]4526
+name = "wasm-bindgen-test"4527
+version = "0.3.78"4528
+source = "registry+https://github.com/rust-lang/crates.io-index"4529
+checksum = "45863ef0bef521c12124eb39d9a38513c47db75f22e503c06beacd40afeb35db"4530
+dependencies = [4531
+ "async-trait",4532
+ "cast",4533
+ "js-sys",4534
+ "libm",4535
+ "minicov",4536
+ "nu-ansi-term",4537
+ "num-traits",4538
+ "oorandom",4539
+ "serde",4540
+ "serde_json",4541
+ "wasm-bindgen",4542
+ "wasm-bindgen-futures",4543
+ "wasm-bindgen-test-macro",4544
+ "wasm-bindgen-test-shared",4545
+]4546
+4547
+[[package]]4548
+name = "wasm-bindgen-test-macro"4549
+version = "0.3.78"4550
+source = "registry+https://github.com/rust-lang/crates.io-index"4551
+checksum = "8c89dcab8b516b6b603baca9d550b7282d68fcc7f367e3956cff7ebf406a3f12"4552
+dependencies = [4553
+ "proc-macro2",4554
+ "quote",4555
+ "syn 3.0.6",4556
+]4557
+4558
+[[package]]4559
+name = "wasm-bindgen-test-shared"4560
+version = "0.2.128"4561
+source = "registry+https://github.com/rust-lang/crates.io-index"4562
+checksum = "f37b4f992cebe528ef34964ae69681ac0fe7080071e7298e46008f9d380302af"4563
+4564
+[[package]]4565
+name = "wasm-streams"4566
+version = "0.5.0"4567
+source = "registry+https://github.com/rust-lang/crates.io-index"4568
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"4569
+dependencies = [4570
+ "futures-util",4571
+ "js-sys",4572
+ "wasm-bindgen",4573
+ "wasm-bindgen-futures",4574
+ "web-sys",4575
+]4576
+4577
+[[package]]4578
+name = "wasm_evt_listener"4579
+version = "0.1.0"4580
+source = "registry+https://github.com/rust-lang/crates.io-index"4581
+checksum = "dc92d6378b411ed94839112a36d9dbc77143451d85b05dfb0cce93a78dab1963"4582
+dependencies = [4583
+ "accessory",4584
+ "derivative",4585
+ "derive_more 1.0.0",4586
+ "fancy_constructor",4587
+ "futures-core",4588
+ "js-sys",4589
+ "smallvec",4590
+ "tokio",4591
+ "wasm-bindgen",4592
+ "web-sys",4593
+]4594
+4595
+[[package]]4596
+name = "web-sys"4597
+version = "0.3.105"4598
+source = "registry+https://github.com/rust-lang/crates.io-index"4599
+checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"4600
+dependencies = [4601
+ "js-sys",4602
+ "wasm-bindgen",4603
+]4604
+4605
+[[package]]4606
+name = "web-time"4607
+version = "1.1.0"4608
+source = "registry+https://github.com/rust-lang/crates.io-index"4609
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"4610
+dependencies = [4611
+ "js-sys",4612
+ "serde",4613
+ "wasm-bindgen",4614
+]4615
+4616
+[[package]]4617
+name = "web_atoms"4618
+version = "0.2.6"4619
+source = "registry+https://github.com/rust-lang/crates.io-index"4620
+checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"4621
+dependencies = [4622
+ "phf",4623
+ "phf_codegen",4624
+ "string_cache",4625
+ "string_cache_codegen",4626
+]4627
+4628
+[[package]]4629
+name = "webpki-root-certs"4630
+version = "1.0.9"4631
+source = "registry+https://github.com/rust-lang/crates.io-index"4632
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"4633
+dependencies = [4634
+ "rustls-pki-types",4635
+]4636
+4637
+[[package]]4638
+name = "webpki-roots"4639
+version = "1.0.9"4640
+source = "registry+https://github.com/rust-lang/crates.io-index"4641
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"4642
+dependencies = [4643
+ "rustls-pki-types",4644
+]4645
+4646
+[[package]]4647
+name = "wildmatch"4648
+version = "2.6.1"4649
+source = "registry+https://github.com/rust-lang/crates.io-index"4650
+checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68"4651
+4652
+[[package]]4653
+name = "winapi"4654
+version = "0.3.9"4655
+source = "registry+https://github.com/rust-lang/crates.io-index"4656
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"4657
+dependencies = [4658
+ "winapi-i686-pc-windows-gnu",4659
+ "winapi-x86_64-pc-windows-gnu",4660
+]4661
+4662
+[[package]]4663
+name = "winapi-i686-pc-windows-gnu"4664
+version = "0.4.0"4665
+source = "registry+https://github.com/rust-lang/crates.io-index"4666
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"4667
+4668
+[[package]]4669
+name = "winapi-util"4670
+version = "0.1.11"4671
+source = "registry+https://github.com/rust-lang/crates.io-index"4672
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"4673
+dependencies = [4674
+ "windows-sys 0.61.2",4675
+]4676
+4677
+[[package]]4678
+name = "winapi-x86_64-pc-windows-gnu"4679
+version = "0.4.0"4680
+source = "registry+https://github.com/rust-lang/crates.io-index"4681
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"4682
+4683
+[[package]]4684
+name = "windows-core"4685
+version = "0.62.2"4686
+source = "registry+https://github.com/rust-lang/crates.io-index"4687
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"4688
+dependencies = [4689
+ "windows-implement",4690
+ "windows-interface",4691
+ "windows-link",4692
+ "windows-result",4693
+ "windows-strings",4694
+]4695
+4696
+[[package]]4697
+name = "windows-implement"4698
+version = "0.60.2"4699
+source = "registry+https://github.com/rust-lang/crates.io-index"4700
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"4701
+dependencies = [4702
+ "proc-macro2",4703
+ "quote",4704
+ "syn 2.0.119",4705
+]4706
+4707
+[[package]]4708
+name = "windows-interface"4709
+version = "0.59.3"4710
+source = "registry+https://github.com/rust-lang/crates.io-index"4711
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"4712
+dependencies = [4713
+ "proc-macro2",4714
+ "quote",4715
+ "syn 2.0.119",4716
+]4717
+4718
+[[package]]4719
+name = "windows-link"4720
+version = "0.2.1"4721
+source = "registry+https://github.com/rust-lang/crates.io-index"4722
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"4723
+4724
+[[package]]4725
+name = "windows-result"4726
+version = "0.4.1"4727
+source = "registry+https://github.com/rust-lang/crates.io-index"4728
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"4729
+dependencies = [4730
+ "windows-link",4731
+]4732
+4733
+[[package]]4734
+name = "windows-strings"4735
+version = "0.5.1"4736
+source = "registry+https://github.com/rust-lang/crates.io-index"4737
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"4738
+dependencies = [4739
+ "windows-link",4740
+]4741
+4742
+[[package]]4743
+name = "windows-sys"4744
+version = "0.52.0"4745
+source = "registry+https://github.com/rust-lang/crates.io-index"4746
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"4747
+dependencies = [4748
+ "windows-targets",4749
+]4750
+4751
+[[package]]4752
+name = "windows-sys"4753
+version = "0.61.2"4754
+source = "registry+https://github.com/rust-lang/crates.io-index"4755
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"4756
+dependencies = [4757
+ "windows-link",4758
+]4759
+4760
+[[package]]4761
+name = "windows-targets"4762
+version = "0.52.6"4763
+source = "registry+https://github.com/rust-lang/crates.io-index"4764
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"4765
+dependencies = [4766
+ "windows_aarch64_gnullvm",4767
+ "windows_aarch64_msvc",4768
+ "windows_i686_gnu",4769
+ "windows_i686_gnullvm",4770
+ "windows_i686_msvc",4771
+ "windows_x86_64_gnu",4772
+ "windows_x86_64_gnullvm",4773
+ "windows_x86_64_msvc",4774
+]4775
+4776
+[[package]]4777
+name = "windows_aarch64_gnullvm"4778
+version = "0.52.6"4779
+source = "registry+https://github.com/rust-lang/crates.io-index"4780
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"4781
+4782
+[[package]]4783
+name = "windows_aarch64_msvc"4784
+version = "0.52.6"4785
+source = "registry+https://github.com/rust-lang/crates.io-index"4786
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"4787
+4788
+[[package]]4789
+name = "windows_i686_gnu"4790
+version = "0.52.6"4791
+source = "registry+https://github.com/rust-lang/crates.io-index"4792
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"4793
+4794
+[[package]]4795
+name = "windows_i686_gnullvm"4796
+version = "0.52.6"4797
+source = "registry+https://github.com/rust-lang/crates.io-index"4798
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"4799
+4800
+[[package]]4801
+name = "windows_i686_msvc"4802
+version = "0.52.6"4803
+source = "registry+https://github.com/rust-lang/crates.io-index"4804
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"4805
+4806
+[[package]]4807
+name = "windows_x86_64_gnu"4808
+version = "0.52.6"4809
+source = "registry+https://github.com/rust-lang/crates.io-index"4810
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"4811
+4812
+[[package]]4813
+name = "windows_x86_64_gnullvm"4814
+version = "0.52.6"4815
+source = "registry+https://github.com/rust-lang/crates.io-index"4816
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"4817
+4818
+[[package]]4819
+name = "windows_x86_64_msvc"4820
+version = "0.52.6"4821
+source = "registry+https://github.com/rust-lang/crates.io-index"4822
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"4823
+4824
+[[package]]4825
+name = "winnow"4826
+version = "1.0.4"4827
+source = "registry+https://github.com/rust-lang/crates.io-index"4828
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"4829
+dependencies = [4830
+ "memchr",4831
+]4832
+4833
+[[package]]4834
+name = "wiremock"4835
+version = "0.6.5"4836
+source = "registry+https://github.com/rust-lang/crates.io-index"4837
+checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"4838
+dependencies = [4839
+ "assert-json-diff",4840
+ "base64 0.22.1",4841
+ "deadpool 0.12.3",4842
+ "futures",4843
+ "http",4844
+ "http-body-util",4845
+ "hyper",4846
+ "hyper-util",4847
+ "log",4848
+ "once_cell",4849
+ "regex",4850
+ "serde",4851
+ "serde_json",4852
+ "tokio",4853
+ "url",4854
+]4855
+4856
+[[package]]4857
+name = "wit-bindgen"4858
+version = "0.57.1"4859
+source = "registry+https://github.com/rust-lang/crates.io-index"4860
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"4861
+4862
+[[package]]4863
+name = "writeable"4864
+version = "0.6.4"4865
+source = "registry+https://github.com/rust-lang/crates.io-index"4866
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"4867
+4868
+[[package]]4869
+name = "x25519-dalek"4870
+version = "2.0.1"4871
+source = "registry+https://github.com/rust-lang/crates.io-index"4872
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"4873
+dependencies = [4874
+ "curve25519-dalek",4875
+ "rand_core 0.6.4",4876
+ "serde",4877
+ "zeroize",4878
+]4879
+4880
+[[package]]4881
+name = "xxhash-rust"4882
+version = "0.8.18"4883
+source = "registry+https://github.com/rust-lang/crates.io-index"4884
+checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6"4885
+4886
+[[package]]4887
+name = "yoke"4888
+version = "0.8.3"4889
+source = "registry+https://github.com/rust-lang/crates.io-index"4890
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"4891
+dependencies = [4892
+ "stable_deref_trait",4893
+ "yoke-derive",4894
+ "zerofrom",4895
+]4896
+4897
+[[package]]4898
+name = "yoke-derive"4899
+version = "0.8.3"4900
+source = "registry+https://github.com/rust-lang/crates.io-index"4901
+checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"4902
+dependencies = [4903
+ "proc-macro2",4904
+ "quote",4905
+ "syn 3.0.6",4906
+ "synstructure",4907
+]4908
+4909
+[[package]]4910
+name = "zerocopy"4911
+version = "0.8.57"4912
+source = "registry+https://github.com/rust-lang/crates.io-index"4913
+checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"4914
+dependencies = [4915
+ "zerocopy-derive",4916
+]4917
+4918
+[[package]]4919
+name = "zerocopy-derive"4920
+version = "0.8.57"4921
+source = "registry+https://github.com/rust-lang/crates.io-index"4922
+checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"4923
+dependencies = [4924
+ "proc-macro2",4925
+ "quote",4926
+ "syn 2.0.119",4927
+]4928
+4929
+[[package]]4930
+name = "zerofrom"4931
+version = "0.1.8"4932
+source = "registry+https://github.com/rust-lang/crates.io-index"4933
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"4934
+dependencies = [4935
+ "zerofrom-derive",4936
+]4937
+4938
+[[package]]4939
+name = "zerofrom-derive"4940
+version = "0.1.8"4941
+source = "registry+https://github.com/rust-lang/crates.io-index"4942
+checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"4943
+dependencies = [4944
+ "proc-macro2",4945
+ "quote",4946
+ "syn 3.0.6",4947
+ "synstructure",4948
+]4949
+4950
+[[package]]4951
+name = "zeroize"4952
+version = "1.9.0"4953
+source = "registry+https://github.com/rust-lang/crates.io-index"4954
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"4955
+dependencies = [4956
+ "zeroize_derive",4957
+]4958
+4959
+[[package]]4960
+name = "zeroize_derive"4961
+version = "1.5.0"4962
+source = "registry+https://github.com/rust-lang/crates.io-index"4963
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"4964
+dependencies = [4965
+ "proc-macro2",4966
+ "quote",4967
+ "syn 2.0.119",4968
+]4969
+4970
+[[package]]4971
+name = "zerotrie"4972
+version = "0.2.5"4973
+source = "registry+https://github.com/rust-lang/crates.io-index"4974
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"4975
+dependencies = [4976
+ "displaydoc",4977
+ "yoke",4978
+ "zerofrom",4979
+]4980
+4981
+[[package]]4982
+name = "zerovec"4983
+version = "0.11.8"4984
+source = "registry+https://github.com/rust-lang/crates.io-index"4985
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"4986
+dependencies = [4987
+ "yoke",4988
+ "zerofrom",4989
+ "zerovec-derive",4990
+]4991
+4992
+[[package]]4993
+name = "zerovec-derive"4994
+version = "0.11.6"4995
+source = "registry+https://github.com/rust-lang/crates.io-index"4996
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"4997
+dependencies = [4998
+ "proc-macro2",4999
+ "quote",5000
+ "syn 3.0.6",5001
+]5002
+5003
+[[package]]5004
+name = "zlib-rs"5005
+version = "0.6.8"5006
+source = "registry+https://github.com/rust-lang/crates.io-index"5007
+checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"5008
+5009
+[[package]]5010
+name = "zmij"5011
+version = "1.0.23"5012
+source = "registry+https://github.com/rust-lang/crates.io-index"5013
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"@@ -0,0 +1,15 @@
1
+[workspace]2
+resolver = "3"3
+members = ["crates/malice-core", "crates/malice-server"]4
+5
+[workspace.dependencies]6
+macltok = { path = "vendor/macltok" }7
+ruma = { version = "0.16", default-features = false }8
+serde = { version = "1", features = ["derive"] }9
+serde_json = "1"10
+tokio = { version = "1" }11
+tracing = "0.1"12
+http = "1"13
+14
+[workspace.lints.rust]15
+unsafe_code = "forbid"@@ -0,0 +1,77 @@
1
+# macltok request, what a host needs to stream large media itself2
+3
+## Problem4
+5
+`ClientHandle::media(&MediaId, MediaSize)` answers with `Media { bytes: Arc<[u8]>, .. }`, the whole6
+file, decrypted and hash-checked, capped by `EngineConfig::max_media_bytes`. That is right for7
+avatars, thumbnails and images. It is wrong for a 20 minute video.8
+9
+- In a browser the whole file sits in wasm linear memory, which never shrinks and tops out at 4 GB10
+ on wasm32. A few long videos and the worker is carrying hundreds of MB it can never give back.11
+- Nothing plays until the last byte has arrived and the hash has been checked, and seeking has to12
+ wait for the same.13
+14
+malice's on-device build serves media through a service worker so `<video>` and `<audio>` get real15
+URLs and Range requests. For large media it wants to answer each Range itself, fetching the matching16
+ciphertext range from the homeserver's authenticated media endpoint and decrypting it in the service17
+worker with WebCrypto AES-CTR (the counter block is the IV plus `offset / 16`, so any 16-byte aligned18
+range decrypts on its own). Nothing is buffered and the browser caches only ciphertext. The hash can19
+only be checked on a whole read, so a partial read plays unverified bytes. malice accepts that.20
+21
+What it can't get today is what the request needs. `MediaRegistration` and `MediaSourceInfo`22
+(`normalise/media.rs`) are `pub(crate)`, and there is no way to read the access token.23
+24
+## Request25
+26
+Two read-only accessors on `ClientHandle`. Suggested shapes, adjust freely.27
+28
+```rust29
+/// where a registered piece of media lives and how to decrypt it, for a host that fetches it30
+/// itself. `None` for an unknown id, same as `media` answering `MediaUnknown`31
+pub fn media_source(&self, media_id: &MediaId, size: MediaSize) -> Option<MediaSourceDetails>;32
+33
+#[non_exhaustive]34
+pub struct MediaSourceDetails {35
+ pub uri: OwnedMxcUri,36
+ pub mimetype: Option<String>,37
+ pub declared_size: Option<u64>,38
+ /// `Some` for an encrypted attachment39
+ pub encryption: Option<AttachmentEncryption>,40
+}41
+42
+#[non_exhaustive]43
+pub struct AttachmentEncryption {44
+ /// the JWK `k`, unpadded url-safe base64, AES-256-CTR45
+ pub key: String,46
+ /// unpadded base64, the initial counter block47
+ pub iv: String,48
+ /// unpadded base64 SHA-256 of the ciphertext, for a host that does read it whole49
+ pub sha256: String,50
+}51
+52
+/// the current access token and the homeserver it belongs to. it changes on every refresh, so53
+/// ask again after a 401 rather than keeping it54
+pub fn media_credentials(&self) -> Option<MediaCredentials>;55
+56
+#[non_exhaustive]57
+pub struct MediaCredentials {58
+ pub homeserver: String,59
+ pub access_token: String,60
+}61
+```62
+63
+Thumbnail sizes with no uploader thumbnail are server-side resizes, which are never encrypted, so64
+`media_source` for `MediaSize::Thumbnail` can answer the uploader's thumbnail when there is one and65
+the full source otherwise, whichever `media` would have used.66
+67
+The token accessor is the sensitive one. It hands a caller the bearer token, which `session_record`68
+already does in serialized form, so it doesn't widen what a host can reach. It is worth a doc line69
+saying the token should stay in memory and only go to the homeserver's media endpoints.70
+71
+## Also worth considering72
+73
+Streaming decryption inside macltok, `media_stream(&MediaId, range) -> impl Stream<Item = Bytes>`,74
+would let every host stream without reimplementing AES-CTR and Range handling, and would keep the75
+token inside macltok. matrix-sdk 0.18 decrypts attachments whole (`Media::get_media_content`), so76
+it would mean reimplementing the attachment decryptor over the raw download. malice will do it77
+host-side for now, so this is not blocking.@@ -0,0 +1,58 @@
1
+# macltok request, pre-share room keys before send2
+3
+## Problem4
+5
+Sending into an encrypted room is slow in malice (and Element X), and under federation lag it can take over a minute. Clients on other SDKs feel instant because they do the key work before the user presses send.6
+7
+matrix-sdk 0.18 does all of it inline on the send path. `SendRawMessageLikeEvent::into_future` (`matrix-sdk-0.18.0/src/room/futures.rs:190-245`) calls the private `ensure_room_encryption_ready` (`futures.rs:523-537`), which runs, in the room's single send-queue worker and before the `/send` PUT8
+9
+1. `sync_members()` if `!are_members_synced()`10
+2. `query_keys_for_untracked_or_dirty_users()`, a `/keys/query` that the homeserver fans out over federation11
+3. `preshare_room_key()` (`room/mod.rs:2396-2431`), which claims one-time keys (`/keys/claim`, 10s server timeout, also federated) and then sends the megolm key to every device as to-device messages, awaiting each12
+13
+Every request uses the default 30s timeout, and the send queue retries with `short_retry` (3 attempts, exponential backoff). A slow remote server stalls the send and everything queued behind it in that room.14
+15
+For comparison, matrix-dart-sdk calls `prepareOutboundGroupSession` unawaited from `Client.setTyping`, so FluffyChat has usually finished steps 1 to 3 by the time the user hits send. matrix-js-sdk exposes `CryptoApi.prepareToEncrypt(room)` for the same purpose. matrix-sdk has the primitive but keeps it private, with this at `room/mod.rs:2392`16
+17
+```rust18
+// TODO: expose this publicly so people can pre-share a group session if19
+// e.g. a user starts to type a message for a room.20
+```21
+22
+## What is reachable today23
+24
+Checked against the 0.18 sources. Only the first step is public.25
+26
+| step | public from `matrix_sdk`? |27
+|---|---|28
+| `Room::sync_members`, `are_members_synced` | yes |29
+| `Room::query_keys_for_untracked_or_dirty_users` | no, private. `Encryption::request_user_identity(user_id)` does a per-user `/keys/query` |30
+| `Client::claim_one_time_keys` | no, `pub(crate)` |31
+| `Room::share_room_key`, `Room::preshare_room_key` | no, private. `OlmMachine::share_room_key` is pub but `Client::olm_machine()` is `pub(crate)` |32
+| `ensure_room_encryption_ready` | no, private free fn |33
+34
+There is no public encrypt-without-send path either. Every caller of `ensure_room_encryption_ready` goes on to send.35
+36
+## Request37
+38
+Expose a command that runs the send path's encryption prep without sending anything, fire and forget, deduplicated per room.39
+40
+```rust41
+impl ClientHandle {42
+ /// no-op for unencrypted rooms, coalesces with an in-flight prepare for the same room43
+ pub async fn prepare_to_send(&self, room_id: OwnedRoomId) -> Result<(), Error>;44
+}45
+```46
+47
+malice would call it when the user starts typing in a room (alongside a typing notice) and probably when a room is opened. A later send then finds members synced, device lists fresh and the outbound session already shared, so only the `/send` PUT remains. Correctness is unchanged, since the send path still runs `ensure_room_encryption_ready` and redoes whatever went stale in between (a member joining after the prepare, say).48
+49
+Suggested route, in order of preference50
+51
+1. Upstream a small matrix-rust-sdk PR making `Room::preshare_room_key` public, or adding `pub async fn Room::prepare_encryption(&self)` that wraps `ensure_room_encryption_ready`. The TODO invites exactly this. Until it is released macltok could carry it via a `[patch.crates-io]` git fork, if that fits its dependency policy.52
+2. A partial warm-up using only public API while the PR is pending. `sync_members()` when not synced, then `request_user_identity` for members whose device lists are unknown, capped (members can be many and each query is a federated round trip). This removes step 1 and the `/keys/query` part of step 2 from the send path, which is the part federation lag hurts most. Steps 3 (`/keys/claim` and the to-device share) stay on the send path. It does not know which users are dirty (only `tracked_users()` is public), so it can over-query. Worth it only if option 1 will take a while.53
+54
+malice will pair it with the existing `ClientHandle::set_typing(room_id, bool)` (`transport.rs:357`), so no new typing API is needed.55
+56
+## Not requested57
+58
+A "send without waiting for fresh device lists" mode. It would encrypt to stale device lists, so new members and new devices miss the key and see undecryptable messages. Pre-sharing gives the same latency without that.@@ -0,0 +1,127 @@
1
+# Malice2
+3
+Malice is a hosted Matrix client with a Rust backend built on macltok and a4
+Svelte frontend. The Nix package serves both the API and the built frontend.5
+6
+## NixOS module7
+8
+Add this repository to your NixOS flake inputs, then include its module:9
+10
+```nix11
+# In your flake.nix12
+inputs.malice.url = "git+file:///absolute/path/to/malice";13
+14
+# In the modules list for your nixosSystem:15
+malice.nixosModules.default16
+```17
+18
+Use a published Git URL instead of the local path if you deploy from another19
+machine. For a local Git flake, add new source files to Git before rebuilding;20
+Nix does not include untracked files.21
+22
+Enable the service in your NixOS configuration. For access from another machine,23
+put it behind an HTTPS reverse proxy; the service binds to localhost and uses24
+Secure session cookies by default:25
+26
+```nix27
+{28
+ services.malice.enable = true;29
+ # Optional: services.malice.port = 3000;30
+31
+ services.nginx.enable = true;32
+ networking.firewall.allowedTCPPorts = [ 80 443 ];33
+ services.nginx.virtualHosts."chat.example.org" = {34
+ enableACME = true;35
+ forceSSL = true;36
+ locations."/" = {37
+ proxyPass = "http://127.0.0.1:3000";38
+ extraConfig = ''39
+ proxy_set_header Host $host;40
+ proxy_buffering off; # Keep event-stream updates timely.41
+ '';42
+ };43
+ };44
+ security.acme.acceptTerms = true;45
+ security.acme.defaults.email = "you@example.org";46
+}47
+```48
+49
+Replace the domain and ACME email, then rebuild your NixOS configuration as50
+usual. Check `systemctl status malice` and visit `https://chat.example.org/`.51
+The module stores the master key and sessions in `/var/lib/malice`; back up that52
+entire directory together. Its `listenAddress`, `port`, `package`, and53
+`openFirewall` options can be overridden. Do not expose the HTTP listener54
+without a trusted TLS proxy.55
+56
+## Client behavior57
+58
+The home list shows non-space rooms (channels); Matrix spaces have their own59
+navigation and show their advertised child rooms. A Matrix homeserver is the60
+network address used at sign-in, not a space. Sending a message returns a queued61
+command, then the timeline shows whether delivery is queued, sending, confirmed62
+by the server, or failed. Opening a room loads its timeline; scrolling up loads63
+older history. Images and avatars are served through authenticated, bounded64
+media endpoints; SVG content is not rendered. Link previews use URL text only65
+and do not fetch third-party pages.66
+67
+The Security panel accepts incoming device-verification requests and supports68
+emoji/number comparison, confirmation, rejection, and recovery-key entry.69
+Recovery keys and encryption state are per Matrix account; preserve the local70
+session data when migrating an installation.71
+72
+## Development73
+74
+From the repository root:75
+76
+```sh77
+nix develop78
+npm --prefix web ci79
+```80
+81
+For live frontend changes, start these in separate terminals inside `nix develop`:82
+83
+```sh84
+MALICE_SESSION_COOKIE_SECURE=false cargo run --bin malice85
+npm --prefix web run dev86
+```87
+88
+Open `http://127.0.0.1:5173/`. Vite proxies `/api` to the Rust server on port89
+3000. The cookie override is for local HTTP development only. By default,90
+Malice accepts HTTPS homeservers; set `MALICE_ALLOW_HTTP_HOMESERVER=true` only91
+when testing against a trusted local HTTP homeserver.92
+93
+To serve the built frontend directly from Rust instead of using Vite:94
+95
+```sh96
+npm --prefix web run build97
+MALICE_SESSION_COOKIE_SECURE=false cargo run --bin malice98
+```99
+100
+Open `http://127.0.0.1:3000/`. Rust uses `web/dist` unless `MALICE_WEB_DIR`101
+points at another built frontend directory. Local session data defaults to102
+`local-data/`; set `MALICE_DATA_DIR` to use another directory. Keep its103
+`master.key` and session files private.104
+105
+## Testing106
+107
+In `nix develop`, after `npm --prefix web ci`:108
+109
+```sh110
+cargo fmt -- --check111
+cargo clippy --all-targets -- -D warnings112
+cargo test --all-targets113
+npm --prefix web exec -- tsc --noEmit -p web/tsconfig.json114
+npm --prefix web run build115
+```116
+117
+The Rust integration test uses a mocked Matrix homeserver; it does not require118
+live account credentials. To verify the deployable package and NixOS module:119
+120
+```sh121
+nix build .#server122
+nix flake check123
+```124
+125
+`nix build` creates `result/bin/malice` with the frontend bundled. On a NixOS126
+host, `curl -fsS http://127.0.0.1:3000/api/health` checks the local backend;127
+opening the proxied URL checks the frontend and TLS setup.@@ -0,0 +1,18 @@
1
+[package]2
+name = "malice-core"3
+version = "0.1.0"4
+edition = "2024"5
+6
+[lib]7
+name = "malice_core"8
+9
+[dependencies]10
+macltok = { workspace = true, features = ["serde", "mirror"] }11
+futures-util = { version = "0.3", default-features = false, features = ["std"] }12
+ruma = { workspace = true }13
+serde = { workspace = true }14
+serde_json = { workspace = true }15
+http = { workspace = true }16
+17
+[lints]18
+workspace = true@@ -0,0 +1,65 @@
1
+use serde::Serialize;2
+3
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]4
+pub enum Failure {5
+ BadRequest,6
+ NotFound,7
+ Conflict,8
+ Busy,9
+ TooLarge,10
+ Unsupported,11
+ EngineStopped,12
+ Upstream,13
+}14
+15
+#[derive(Debug, Clone, Serialize)]16
+pub struct CoreError {17
+ pub kind: Failure,18
+ pub message: String,19
+}20
+21
+impl CoreError {22
+ pub fn new(kind: Failure, message: impl Into<String>) -> Self {23
+ Self {24
+ kind,25
+ message: message.into(),26
+ }27
+ }28
+29
+ pub fn bad_request(message: impl Into<String>) -> Self {30
+ Self::new(Failure::BadRequest, message)31
+ }32
+33
+ pub fn not_found(message: impl Into<String>) -> Self {34
+ Self::new(Failure::NotFound, message)35
+ }36
+37
+ pub fn unsupported(message: impl Into<String>) -> Self {38
+ Self::new(Failure::Unsupported, message)39
+ }40
+}41
+42
+impl From<macltok::Error> for CoreError {43
+ fn from(error: macltok::Error) -> Self {44
+ use macltok::ErrorKind;45
+ let kind = match error.kind {46
+ ErrorKind::Interrupted => {47
+ return Self::new(48
+ Failure::EngineStopped,49
+ "the sync engine stopped; sign in again",50
+ );51
+ }52
+ ErrorKind::QueueFull | ErrorKind::RoomBusy => Failure::Busy,53
+ ErrorKind::RoomUnknown | ErrorKind::MediaUnknown | ErrorKind::MediaUnavailable => {54
+ Failure::NotFound55
+ }56
+ ErrorKind::MediaTooLarge => Failure::TooLarge,57
+ ErrorKind::Invalid | ErrorKind::EmptyBody => Failure::BadRequest,58
+ ErrorKind::RoomNotOpen59
+ | ErrorKind::TimelineNotOpen60
+ | ErrorKind::VerificationInactive => Failure::Conflict,61
+ _ => Failure::Upstream,62
+ };63
+ Self::new(kind, error.message)64
+ }65
+}@@ -0,0 +1,179 @@
1
+use std::collections::HashMap;2
+3
+use futures_util::{Stream, StreamExt, stream};4
+use macltok::{5
+ ConnectionState, EncryptionState, MirrorHandle, RoomSummary, SessionStatus, State, StateChange,6
+ TimelineChange, TimelineId, TimelineItem, UploadOutcome, VerificationState, imbl::Vector,7
+};8
+use serde::Serialize;9
+10
+#[derive(Debug, Clone, Serialize)]11
+#[serde(tag = "type", rename_all = "snake_case")]12
+pub enum WireEvent {13
+ Snapshot {14
+ rooms: Vec<RoomSummary>,15
+ timelines: HashMap<String, Vector<TimelineItem>>,16
+ connection: String,17
+ encryption: Option<EncryptionState>,18
+ verification: Option<VerificationState>,19
+ },20
+ Rooms {21
+ rooms: Vec<RoomSummary>,22
+ },23
+ Timeline {24
+ room_id: String,25
+ items: Vector<TimelineItem>,26
+ },27
+ TimelineClosed {28
+ room_id: String,29
+ },30
+ Connection {31
+ state: String,32
+ },33
+ Encryption {34
+ state: EncryptionState,35
+ },36
+ Verification {37
+ state: VerificationState,38
+ },39
+ Error {40
+ message: String,41
+ },42
+ /// Emitted directly by a Host when an account's engine ends; the hosted43
+ /// SSE stream never produces this, since it has no equivalent of an44
+ /// account disappearing out from under a live subscription.45
+ SessionEnded {46
+ retain: SessionRetain,47
+ },48
+}49
+50
+/// `macltok::Retain`'s wire form. macltok's own enum has no `rename_all`,51
+/// so this exists to give it one.52
+#[derive(Debug, Clone, Copy, Serialize)]53
+#[serde(rename_all = "snake_case")]54
+pub enum SessionRetain {55
+ Nothing,56
+ Store,57
+ RecordAndStore,58
+}59
+60
+impl From<macltok::Retain> for SessionRetain {61
+ fn from(retain: macltok::Retain) -> Self {62
+ match retain {63
+ macltok::Retain::Nothing => Self::Nothing,64
+ macltok::Retain::Store => Self::Store,65
+ macltok::Retain::RecordAndStore => Self::RecordAndStore,66
+ }67
+ }68
+}69
+70
+/// A fresh subscriber's first frame, built from the mirror's current state.71
+pub fn snapshot_event(state: &State) -> WireEvent {72
+ let timelines = state73
+ .timelines74
+ .iter()75
+ .filter_map(|(timeline_id, timeline)| match timeline_id {76
+ TimelineId::Room(room_id) => {77
+ Some((room_id.as_str().to_owned(), timeline.items.clone()))78
+ }79
+ _ => None,80
+ })81
+ .collect();82
+ WireEvent::Snapshot {83
+ rooms: state.rooms.iter().cloned().collect(),84
+ timelines,85
+ connection: connection_label(&state.connection),86
+ encryption: state.encryption.clone(),87
+ verification: state.verification.clone(),88
+ }89
+}90
+91
+fn error(message: &str) -> WireEvent {92
+ WireEvent::Error {93
+ message: message.to_owned(),94
+ }95
+}96
+97
+pub fn wire_events(change: &StateChange) -> Vec<WireEvent> {98
+ match change {99
+ StateChange::State(state) => {100
+ let mut events = vec![snapshot_event(state)];101
+ if let Some(message) = &state.session_save_failure {102
+ events.push(WireEvent::Error {103
+ message: message.clone(),104
+ });105
+ }106
+ events107
+ }108
+ StateChange::Rooms(rooms) => vec![WireEvent::Rooms {109
+ rooms: rooms.iter().cloned().collect(),110
+ }],111
+ StateChange::Timeline {112
+ timeline_id: TimelineId::Room(room_id),113
+ change,114
+ } => match change {115
+ TimelineChange::Reset { items } | TimelineChange::Changed { items, .. } => {116
+ vec![WireEvent::Timeline {117
+ room_id: room_id.as_str().to_owned(),118
+ items: items.clone(),119
+ }]120
+ }121
+ TimelineChange::Closed => vec![WireEvent::TimelineClosed {122
+ room_id: room_id.as_str().to_owned(),123
+ }],124
+ _ => vec![],125
+ },126
+ StateChange::Timeline { .. } => vec![],127
+ StateChange::Connection(state) => vec![WireEvent::Connection {128
+ state: connection_label(state),129
+ }],130
+ StateChange::Encryption(state) => vec![WireEvent::Encryption {131
+ state: state.clone(),132
+ }],133
+ StateChange::Verification(state) => vec![WireEvent::Verification {134
+ state: state.clone(),135
+ }],136
+ StateChange::Failed(failure) => vec![error(&failure.message)],137
+ StateChange::SessionSaveFailed(_) => {138
+ vec![error(139
+ "saving the session failed, you may need to sign in again",140
+ )]141
+ }142
+ StateChange::Session(143
+ SessionStatus::AccessRevoked | SessionStatus::SoftLoggedOut { .. },144
+ ) => {145
+ vec![error("session rejected by the server; sign in again")]146
+ }147
+ StateChange::UploadFinished { outcome, .. } => match outcome {148
+ UploadOutcome::Sent { .. } => vec![],149
+ UploadOutcome::Cancelled => vec![error("upload cancelled")],150
+ UploadOutcome::RoomForgotten => vec![error("upload cancelled, room forgotten")],151
+ UploadOutcome::Missed => vec![error("upload status unknown")],152
+ _ => vec![error("upload did not complete")],153
+ },154
+ StateChange::Upload { upload, .. } => match &upload.failure {155
+ Some(failure) if !failure.will_retry => vec![error("upload failed")],156
+ _ => vec![],157
+ },158
+ _ => vec![],159
+ }160
+}161
+162
+/// Snapshot first, then updates with no gap. Ends once the mirror stops.163
+pub fn subscribe(mirror: &MirrorHandle) -> impl Stream<Item = WireEvent> + use<> {164
+ mirror165
+ .subscribe()166
+ .flat_map(|update| stream::iter(wire_events(&update.change)))167
+}168
+169
+pub fn connection_label(state: &ConnectionState) -> String {170
+ match state {171
+ ConnectionState::Idle => "idle",172
+ ConnectionState::Connecting => "connecting",173
+ ConnectionState::Online => "online",174
+ ConnectionState::Offline => "offline",175
+ ConnectionState::Stopped => "stopped",176
+ _ => "unknown",177
+ }178
+ .to_owned()179
+}@@ -0,0 +1,6 @@
1
+pub mod error;2
+pub mod feed;3
+pub mod login;4
+pub mod media;5
+pub mod parse;6
+pub mod request;@@ -0,0 +1,95 @@
1
+use serde::Deserialize;2
+3
+use crate::error::CoreError;4
+5
+#[derive(Debug, Deserialize)]6
+pub struct LoginInput {7
+ #[serde(default)]8
+ pub homeserver: String,9
+ #[serde(default)]10
+ pub username: String,11
+ #[serde(default)]12
+ pub password: String,13
+}14
+15
+impl LoginInput {16
+ pub fn validate(self, allow_http: bool) -> Result<LoginInput, CoreError> {17
+ let homeserver = normalise_homeserver(&self.homeserver, allow_http)?;18
+ let username = self.username.trim();19
+ if username.is_empty() {20
+ return Err(CoreError::bad_request("username is required"));21
+ }22
+ if self.password.is_empty() {23
+ return Err(CoreError::bad_request("password is required"));24
+ }25
+ Ok(LoginInput {26
+ homeserver,27
+ username: username.to_owned(),28
+ password: self.password,29
+ })30
+ }31
+}32
+33
+pub fn normalise_homeserver(raw: &str, allow_http: bool) -> Result<String, CoreError> {34
+ let trimmed = raw.trim();35
+ if trimmed.is_empty() {36
+ return Err(CoreError::bad_request("homeserver is required"));37
+ }38
+ if trimmed.contains(char::is_whitespace) {39
+ return Err(CoreError::bad_request("homeserver must not contain spaces"));40
+ }41
+ let with_scheme = if trimmed.contains("://") {42
+ trimmed.to_owned()43
+ } else {44
+ format!("https://{trimmed}")45
+ };46
+ let uri: http::Uri = with_scheme47
+ .parse()48
+ .map_err(|_| CoreError::bad_request("invalid homeserver"))?;49
+ let scheme = uri50
+ .scheme_str()51
+ .ok_or_else(|| CoreError::bad_request("invalid homeserver"))?;52
+ if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {53
+ return Err(CoreError::bad_request("homeserver must use http(s)"));54
+ }55
+ let authority = uri56
+ .authority()57
+ .ok_or_else(|| CoreError::bad_request("homeserver must include a host"))?;58
+ if authority.as_str().contains('@') || authority.host().is_empty() {59
+ return Err(CoreError::bad_request("homeserver must include a host"));60
+ }61
+ if scheme.eq_ignore_ascii_case("http") {62
+ let host = authority.host().trim_end_matches('.');63
+ let loopback = host.eq_ignore_ascii_case("localhost")64
+ || host == "127.0.0.1"65
+ || host == "[::1]"66
+ || host == "::1";67
+ if !(allow_http && loopback) {68
+ return Err(CoreError::bad_request(69
+ "insecure http homeservers are not allowed",70
+ ));71
+ }72
+ }73
+ Ok(with_scheme)74
+}75
+76
+/// macltok's `AccountError` displays only its context, the homeserver's answer is in the sources77
+pub fn describe_error(error: &(dyn std::error::Error + 'static)) -> String {78
+ let mut message = error.to_string();79
+ let mut source = error.source();80
+ while let Some(cause) = source {81
+ message.push_str(", ");82
+ message.push_str(&cause.to_string());83
+ source = cause.source();84
+ }85
+ message86
+}87
+88
+pub fn sanitise_auth_error(message: &str, password: &str) -> String {89
+ let message = if password.is_empty() {90
+ message.to_owned()91
+ } else {92
+ message.replace(password, "[redacted]")93
+ };94
+ message.chars().take(400).collect()95
+}@@ -0,0 +1,45 @@
1
+pub const AVATAR_SIZE: u32 = 256;2
+pub const THUMBNAIL_SIZE: u32 = 800;3
+4
+pub fn image_content_type(bytes: &[u8]) -> Option<&'static str> {5
+ if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {6
+ Some("image/png")7
+ } else if bytes.starts_with(b"\xff\xd8\xff") {8
+ Some("image/jpeg")9
+ } else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {10
+ Some("image/gif")11
+ } else if bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WEBP" {12
+ Some("image/webp")13
+ } else if bytes.len() >= 1214
+ && &bytes[4..8] == b"ftyp"15
+ && (&bytes[8..12] == b"avif" || &bytes[8..12] == b"avis")16
+ {17
+ Some("image/avif")18
+ } else {19
+ None20
+ }21
+}22
+23
+pub fn attachment_content_type(bytes: &[u8]) -> Option<&'static str> {24
+ if let Some(image) = image_content_type(bytes) {25
+ return Some(image);26
+ }27
+ if bytes.starts_with(b"\x1a\x45\xdf\xa3") {28
+ Some("video/webm")29
+ } else if bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WAVE" {30
+ Some("audio/wav")31
+ } else if bytes.starts_with(b"OggS") {32
+ Some("audio/ogg")33
+ } else if bytes.starts_with(b"ID3") || bytes.starts_with(b"\xff\xfb") {34
+ Some("audio/mpeg")35
+ } else if bytes.len() >= 1236
+ && &bytes[4..8] == b"ftyp"37
+ && [b"isom", b"iso2", b"mp41", b"mp42", b"avc1"]38
+ .iter()39
+ .any(|brand| &bytes[8..12] == brand.as_slice())40
+ {41
+ Some("video/mp4")42
+ } else {43
+ None44
+ }45
+}@@ -0,0 +1,25 @@
1
+use crate::error::CoreError;2
+3
+pub fn parse_room_id(raw: &str) -> Result<ruma::OwnedRoomId, CoreError> {4
+ let trimmed = raw.trim();5
+ if trimmed.is_empty() {6
+ return Err(CoreError::bad_request("room_id is required"));7
+ }8
+ ruma::OwnedRoomId::try_from(trimmed).map_err(|_| CoreError::bad_request("invalid room_id"))9
+}10
+11
+pub fn parse_event_id(raw: &str) -> Result<ruma::OwnedEventId, CoreError> {12
+ let trimmed = raw.trim();13
+ if trimmed.is_empty() {14
+ return Err(CoreError::bad_request("event_id is required"));15
+ }16
+ ruma::OwnedEventId::try_from(trimmed).map_err(|_| CoreError::bad_request("invalid event_id"))17
+}18
+19
+pub fn parse_room_or_alias(raw: &str) -> Result<ruma::OwnedRoomOrAliasId, CoreError> {20
+ let trimmed = raw.trim();21
+ if trimmed.is_empty() {22
+ return Err(CoreError::bad_request("room is required"));23
+ }24
+ ruma::OwnedRoomOrAliasId::try_from(trimmed).map_err(|_| CoreError::bad_request("invalid room"))25
+}@@ -0,0 +1,336 @@
1
+use macltok::{2
+ AttachmentUpload, ClientHandle, CreateRoomOptions, MediaId, MediaSize, MemberFilter,3
+ MessageContent, MirrorHandle, TimelineId, VerificationAction, VerificationTarget,4
+};5
+use serde::Deserialize;6
+use serde_json::json;7
+8
+use crate::{9
+ error::CoreError,10
+ media::{AVATAR_SIZE, THUMBNAIL_SIZE, attachment_content_type, image_content_type},11
+ parse::{parse_event_id, parse_room_id, parse_room_or_alias},12
+};13
+14
+#[derive(Debug, Deserialize)]15
+#[serde(tag = "op", rename_all = "snake_case")]16
+pub enum Request {17
+ Rooms,18
+ Members {19
+ #[serde(default)]20
+ room_id: String,21
+ },22
+ Open {23
+ #[serde(default)]24
+ room_id: String,25
+ },26
+ Close {27
+ #[serde(default)]28
+ room_id: String,29
+ },30
+ Send {31
+ #[serde(default)]32
+ room_id: String,33
+ #[serde(default)]34
+ body: String,35
+ },36
+ Upload {37
+ room_id: String,38
+ filename: String,39
+ mimetype: String,40
+ #[serde(skip)]41
+ bytes: Vec<u8>,42
+ },43
+ Paginate {44
+ #[serde(default)]45
+ room_id: String,46
+ },47
+ Join {48
+ #[serde(default)]49
+ room: String,50
+ },51
+ Create {52
+ #[serde(default)]53
+ name: String,54
+ #[serde(default)]55
+ topic: Option<String>,56
+ },57
+ Leave {58
+ #[serde(default)]59
+ room_id: String,60
+ },61
+ React {62
+ #[serde(default)]63
+ room_id: String,64
+ #[serde(default)]65
+ event_id: String,66
+ #[serde(default)]67
+ key: String,68
+ },69
+ Read {70
+ #[serde(default)]71
+ room_id: String,72
+ #[serde(default)]73
+ event_id: String,74
+ },75
+ Profile,76
+ SpaceChildren {77
+ #[serde(default)]78
+ room_id: String,79
+ },80
+ Encryption,81
+ Recover {82
+ recovery_key: String,83
+ },84
+ StartVerification,85
+ Verify {86
+ action: String,87
+ },88
+ Avatar {89
+ mxc: String,90
+ },91
+ Media {92
+ media_id: String,93
+ size: String,94
+ },95
+ Typing {96
+ #[serde(default)]97
+ room_id: String,98
+ #[serde(default)]99
+ typing: bool,100
+ },101
+}102
+103
+impl Request {104
+ pub fn upload(room_id: String, filename: String, mimetype: String, bytes: Vec<u8>) -> Request {105
+ Request::Upload {106
+ room_id,107
+ filename,108
+ mimetype,109
+ bytes,110
+ }111
+ }112
+}113
+114
+pub enum Reply {115
+ Done,116
+ Json(serde_json::Value),117
+ Avatar {118
+ bytes: Vec<u8>,119
+ content_type: &'static str,120
+ },121
+ Attachment {122
+ bytes: Vec<u8>,123
+ content_type: Option<&'static str>,124
+ },125
+}126
+127
+pub async fn dispatch(128
+ handle: &ClientHandle,129
+ mirror: &MirrorHandle,130
+ request: Request,131
+) -> Result<Reply, CoreError> {132
+ match request {133
+ Request::Rooms => {134
+ let rooms = handle.rooms().await;135
+ Ok(Reply::Json(json!({ "rooms": rooms })))136
+ }137
+ Request::Members { room_id } => {138
+ let room_id = parse_room_id(&room_id)?;139
+ let members = handle.members(&room_id, MemberFilter::Active).await?;140
+ Ok(Reply::Json(json!({ "members": members })))141
+ }142
+ Request::Open { room_id } => {143
+ let room_id = parse_room_id(&room_id)?;144
+ handle.open_room(room_id).await?;145
+ Ok(Reply::Done)146
+ }147
+ Request::Close { room_id } => {148
+ let room_id = parse_room_id(&room_id)?;149
+ handle.close_room(room_id).await?;150
+ Ok(Reply::Done)151
+ }152
+ Request::Send { room_id, body } => {153
+ let room_id = parse_room_id(&room_id)?;154
+ if body.trim().is_empty() {155
+ return Err(CoreError::bad_request("body must not be empty"));156
+ }157
+ if body.len() > 64 * 1024 {158
+ return Err(CoreError::bad_request("body is too large"));159
+ }160
+ handle161
+ .send_message(TimelineId::Room(room_id), MessageContent::text(body))162
+ .await?;163
+ Ok(Reply::Done)164
+ }165
+ Request::Upload {166
+ room_id,167
+ filename,168
+ mimetype,169
+ bytes,170
+ } => {171
+ let room_id = parse_room_id(&room_id)?;172
+ if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {173
+ return Err(CoreError::bad_request(174
+ "file must be between 1 byte and 20 MiB",175
+ ));176
+ }177
+ let upload = AttachmentUpload::new(room_id, filename, mimetype, bytes);178
+ handle.upload(upload).await?;179
+ Ok(Reply::Done)180
+ }181
+ Request::Paginate { room_id } => {182
+ let room_id = parse_room_id(&room_id)?;183
+ handle.paginate(TimelineId::Room(room_id), 30).await?;184
+ Ok(Reply::Done)185
+ }186
+ Request::Join { room } => {187
+ let room = parse_room_or_alias(&room)?;188
+ let room_id = handle.join_room(room, Vec::new()).await?;189
+ Ok(Reply::Json(json!({ "room_id": room_id })))190
+ }191
+ Request::Create { name, topic } => {192
+ let name = name.trim();193
+ if name.is_empty() {194
+ return Err(CoreError::bad_request("name is required"));195
+ }196
+ if name.len() > 255 {197
+ return Err(CoreError::bad_request("name is too long"));198
+ }199
+ if topic.as_deref().is_some_and(|topic| topic.len() > 1024) {200
+ return Err(CoreError::bad_request("topic is too long"));201
+ }202
+ let mut options = CreateRoomOptions::default().with_name(name);203
+ if let Some(topic) = topic204
+ .as_deref()205
+ .map(str::trim)206
+ .filter(|topic| !topic.is_empty())207
+ {208
+ options = options.with_topic(topic);209
+ }210
+ let room_id = handle.create_room(options).await?;211
+ Ok(Reply::Json(json!({ "room_id": room_id })))212
+ }213
+ Request::Leave { room_id } => {214
+ let room_id = parse_room_id(&room_id)?;215
+ handle.leave_room(room_id).await?;216
+ Ok(Reply::Done)217
+ }218
+ Request::React {219
+ room_id,220
+ event_id,221
+ key,222
+ } => {223
+ let room_id = parse_room_id(&room_id)?;224
+ let event_id = parse_event_id(&event_id)?;225
+ let key = key.trim();226
+ if key.is_empty() || key.chars().count() > 32 {227
+ return Err(CoreError::bad_request("key must be 1..32 characters"));228
+ }229
+ handle230
+ .toggle_reaction(TimelineId::Room(room_id), event_id, key)231
+ .await?;232
+ Ok(Reply::Done)233
+ }234
+ Request::Read { room_id, event_id } => {235
+ let room_id = parse_room_id(&room_id)?;236
+ let event_id = parse_event_id(&event_id)?;237
+ handle238
+ .mark_read(TimelineId::Room(room_id), event_id)239
+ .await?;240
+ Ok(Reply::Done)241
+ }242
+ Request::Profile => {243
+ let profile = handle.own_profile().await?;244
+ Ok(Reply::Json(json!({245
+ "display_name": profile.display_name,246
+ "avatar_url": profile.avatar,247
+ })))248
+ }249
+ Request::SpaceChildren { room_id } => {250
+ let room_id = parse_room_id(&room_id)?;251
+ let rooms = handle.rooms().await;252
+ match rooms.into_iter().find(|room| room.room_id == room_id) {253
+ Some(space) => Ok(Reply::Json(json!({ "room_ids": space.children }))),254
+ None => Err(CoreError::not_found(255
+ "room is not available in the local store",256
+ )),257
+ }258
+ }259
+ Request::Encryption => {260
+ let state = mirror.snapshot();261
+ Ok(Reply::Json(json!({262
+ "state": state.encryption,263
+ "verification": state.verification,264
+ })))265
+ }266
+ Request::Recover { recovery_key } => {267
+ if recovery_key.trim().is_empty() {268
+ return Err(CoreError::bad_request("recovery_key is required"));269
+ }270
+ handle.recover(recovery_key).await?;271
+ Ok(Reply::Done)272
+ }273
+ Request::StartVerification => {274
+ handle275
+ .start_verification(VerificationTarget::OwnIdentity)276
+ .await?;277
+ Ok(Reply::Done)278
+ }279
+ Request::Verify { action } => {280
+ let action = match action.as_str() {281
+ "Accept" => VerificationAction::Accept,282
+ "Confirm" => VerificationAction::Confirm,283
+ "Reject" => VerificationAction::Reject,284
+ "Cancel" => VerificationAction::Cancel,285
+ _ => return Err(CoreError::bad_request("invalid verification action")),286
+ };287
+ handle.verify(action).await?;288
+ Ok(Reply::Done)289
+ }290
+ Request::Avatar { mxc } => {291
+ let mxc_ref = <&ruma::MxcUri>::from(mxc.as_str());292
+ if !mxc_ref.is_valid() {293
+ return Err(CoreError::bad_request("invalid mxc URI"));294
+ }295
+ let size = MediaSize::Thumbnail {296
+ width: AVATAR_SIZE,297
+ height: AVATAR_SIZE,298
+ };299
+ let bytes = handle.mxc_media(mxc_ref, size).await?.bytes.to_vec();300
+ let Some(content_type) = image_content_type(&bytes) else {301
+ return Err(CoreError::unsupported(302
+ "MXC content is not a supported image",303
+ ));304
+ };305
+ Ok(Reply::Avatar {306
+ bytes,307
+ content_type,308
+ })309
+ }310
+ Request::Media { media_id, size } => {311
+ if media_id.is_empty() || media_id.len() > 512 {312
+ return Err(CoreError::bad_request("invalid media_id"));313
+ }314
+ let media_id = MediaId::from(media_id);315
+ let size = match size.as_str() {316
+ "Thumbnail" => MediaSize::Thumbnail {317
+ width: THUMBNAIL_SIZE,318
+ height: THUMBNAIL_SIZE,319
+ },320
+ "Full" => MediaSize::Full,321
+ _ => return Err(CoreError::bad_request("invalid size")),322
+ };323
+ let bytes = handle.media(&media_id, size).await?.bytes.to_vec();324
+ let content_type = attachment_content_type(&bytes);325
+ Ok(Reply::Attachment {326
+ bytes,327
+ content_type,328
+ })329
+ }330
+ Request::Typing { room_id, typing } => {331
+ let room_id = parse_room_id(&room_id)?;332
+ handle.set_typing(room_id, typing).await?;333
+ Ok(Reply::Done)334
+ }335
+ }336
+}@@ -0,0 +1,40 @@
1
+[package]2
+name = "malice"3
+version = "0.1.0"4
+edition = "2024"5
+6
+[[bin]]7
+name = "malice"8
+path = "src/main.rs"9
+10
+[lib]11
+name = "malice"12
+path = "src/lib.rs"13
+14
+[dependencies]15
+malice-core = { path = "../malice-core" }16
+macltok = { workspace = true, features = ["sqlite", "serde", "mirror"] }17
+axum = { version = "0.8", features = ["macros", "multipart"] }18
+futures-util = { version = "0.3", default-features = false, features = ["std"] }19
+hex = "0.4"20
+listenfd = "1"21
+rand_core = { version = "0.6", features = ["getrandom"] }22
+ruma = { workspace = true }23
+serde = { workspace = true }24
+serde_json = { workspace = true }25
+sha2 = "0.10"26
+tokio = { workspace = true, features = ["full"] }27
+tower-http = { version = "0.6", features = ["fs", "trace"] }28
+tracing = { workspace = true }29
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }30
+31
+[dev-dependencies]32
+http-body-util = "0.1"33
+tower = { version = "0.5", features = ["util"] }34
+matrix-sdk = { version = "0.18", default-features = false, features = ["testing"] }35
+reqwest = { version = "0.13", default-features = false, features = ["json", "query", "rustls"] }36
+tempfile = "3"37
+wiremock = "0.6"38
+39
+[lints]40
+workspace = true@@ -0,0 +1,111 @@
1
+use axum::response::Json;2
+use axum::{3
+ extract::Request,4
+ http::{HeaderMap, Method, StatusCode, header},5
+ middleware::Next,6
+ response::{IntoResponse, Response},7
+};8
+use serde_json::json;9
+10
+pub const COOKIE_NAME: &str = "malice_session";11
+const COOKIE_MAX_AGE: u32 = 2_592_000;12
+13
+/// Parse the opaque session id from the `Cookie` header.14
+///15
+/// Returns `None` when the cookie is absent or malformed. The value is16
+/// restricted to a safe alphabet so it can be used as a file name.17
+pub fn session_sid(headers: &HeaderMap) -> Option<String> {18
+ for value in headers.get_all(header::COOKIE) {19
+ let Ok(raw) = value.to_str() else {20
+ continue;21
+ };22
+ for part in raw.split(';') {23
+ let part = part.trim();24
+ let Some((name, val)) = part.split_once('=') else {25
+ continue;26
+ };27
+ if name.trim() != COOKIE_NAME {28
+ continue;29
+ }30
+ let val = val.trim().trim_matches('"');31
+ if valid_sid(val) {32
+ return Some(val.to_owned());33
+ }34
+ }35
+ }36
+ None37
+}38
+39
+pub fn valid_sid(value: &str) -> bool {40
+ !value.is_empty()41
+ && value.len() <= 12842
+ && value43
+ .chars()44
+ .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')45
+}46
+47
+pub fn set_cookie_value(sid: &str, secure: bool) -> String {48
+ let mut cookie =49
+ format!("{COOKIE_NAME}={sid}; Path=/; HttpOnly; SameSite=Lax; Max-Age={COOKIE_MAX_AGE}");50
+ if secure {51
+ cookie.push_str("; Secure");52
+ }53
+ cookie54
+}55
+56
+pub fn clear_cookie_value(secure: bool) -> String {57
+ let mut cookie = format!("{COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");58
+ if secure {59
+ cookie.push_str("; Secure");60
+ }61
+ cookie62
+}63
+64
+/// Reject cross-origin mutating API requests. Port is part of an origin:65
+/// another app on the same host must not be able to drive this user's cookie.66
+/// The dev proxy preserves Host, so its forwarded requests remain same-origin.67
+pub async fn csrf_guard(request: Request, next: Next) -> Response {68
+ let method = request.method().clone();69
+ let path = request.uri().path().to_owned();70
+ let is_api = path == "/api" || path.starts_with("/api/");71
+ let is_mutating = method != Method::GET && method != Method::HEAD && method != Method::OPTIONS;72
+ if !(is_api && is_mutating) {73
+ return next.run(request).await;74
+ }75
+ let headers = request.headers();76
+ // Host only: X-Forwarded-Host is client-controlled and must not decide77
+ // same-origin. The Host the backend received is what the browser used,78
+ // which is exactly what Origin must match.79
+ let host = headers80
+ .get(header::HOST)81
+ .and_then(|value| value.to_str().ok())82
+ .unwrap_or("");83
+ let origin = headers84
+ .get(header::ORIGIN)85
+ .and_then(|value| value.to_str().ok());86
+ let referer = headers87
+ .get(header::REFERER)88
+ .and_then(|value| value.to_str().ok());89
+ let allowed = match (origin, referer) {90
+ (Some(value), _) => same_authority(value, host),91
+ (None, Some(value)) => same_authority(value, host),92
+ (None, None) => true,93
+ };94
+ if !allowed {95
+ return (96
+ StatusCode::FORBIDDEN,97
+ Json(json!({"message": "origin check failed"})),98
+ )99
+ .into_response();100
+ }101
+ next.run(request).await102
+}103
+104
+fn same_authority(source: &str, host: &str) -> bool {105
+ source.parse::<axum::http::Uri>().ok().is_some_and(|uri| {106
+ matches!(uri.scheme_str(), Some("http" | "https"))107
+ && uri108
+ .authority()109
+ .is_some_and(|authority| authority.as_str().eq_ignore_ascii_case(host))110
+ })111
+}@@ -0,0 +1,110 @@
1
+use axum::{2
+ http::{HeaderMap, StatusCode, header},3
+ response::{IntoResponse, Response},4
+};5
+use malice_core::error::{CoreError, Failure};6
+use serde::Serialize;7
+use serde_json::json;8
+9
+use crate::csrf::clear_cookie_value;10
+11
+/// A JSON API failure: `{ "message": ... }` with a meaningful status code.12
+pub struct ApiError {13
+ status: StatusCode,14
+ message: String,15
+ /// When set, the response clears the session cookie (stale credential).16
+ clear_session: Option<bool>,17
+}18
+19
+impl ApiError {20
+ pub fn new(status: StatusCode, message: impl Into<String>) -> Self {21
+ Self {22
+ status,23
+ message: message.into(),24
+ clear_session: None,25
+ }26
+ }27
+28
+ pub fn bad_request(message: impl Into<String>) -> Self {29
+ Self::new(StatusCode::BAD_REQUEST, message)30
+ }31
+32
+ pub fn unauthorized(message: impl Into<String>) -> Self {33
+ Self::new(StatusCode::UNAUTHORIZED, message)34
+ }35
+36
+ /// 401 that also expires the session cookie.37
+ pub fn unauthorized_stale(message: impl Into<String>, cookie_secure: bool) -> Self {38
+ Self {39
+ status: StatusCode::UNAUTHORIZED,40
+ message: message.into(),41
+ clear_session: Some(cookie_secure),42
+ }43
+ }44
+45
+ pub fn not_found(message: impl Into<String>) -> Self {46
+ Self::new(StatusCode::NOT_FOUND, message)47
+ }48
+49
+ pub fn engine_stopped() -> Self {50
+ Self::new(51
+ StatusCode::BAD_GATEWAY,52
+ "the sync engine stopped; sign in again",53
+ )54
+ }55
+}56
+57
+impl From<CoreError> for ApiError {58
+ fn from(error: CoreError) -> Self {59
+ let status = match error.kind {60
+ Failure::BadRequest => StatusCode::BAD_REQUEST,61
+ Failure::NotFound => StatusCode::NOT_FOUND,62
+ Failure::Conflict => StatusCode::CONFLICT,63
+ Failure::Busy => StatusCode::TOO_MANY_REQUESTS,64
+ Failure::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,65
+ Failure::Unsupported => StatusCode::UNSUPPORTED_MEDIA_TYPE,66
+ Failure::EngineStopped => return Self::engine_stopped(),67
+ Failure::Upstream => StatusCode::BAD_GATEWAY,68
+ };69
+ Self::new(status, error.message)70
+ }71
+}72
+73
+impl IntoResponse for ApiError {74
+ fn into_response(self) -> Response {75
+ let mut headers = HeaderMap::new();76
+ if let Some(secure) = self.clear_session77
+ && let Ok(value) = clear_cookie_value(secure).parse()78
+ {79
+ headers.insert(header::SET_COOKIE, value);80
+ }81
+ (82
+ self.status,83
+ headers,84
+ axum::Json(json!({ "message": self.message })),85
+ )86
+ .into_response()87
+ }88
+}89
+90
+/// 200 JSON response, optionally refreshing the session cookie.91
+pub fn json_ok<T: Serialize>(value: &T, set_cookie: Option<String>) -> Response {92
+ let mut headers = HeaderMap::new();93
+ if let Some(cookie) = set_cookie94
+ && let Ok(value) = cookie.parse()95
+ {96
+ headers.insert(header::SET_COOKIE, value);97
+ }98
+ (StatusCode::OK, headers, axum::Json(json!(value))).into_response()99
+}100
+101
+/// 204 response, optionally clearing the session cookie.102
+pub fn no_content(clear_cookie: Option<String>) -> Response {103
+ let mut headers = HeaderMap::new();104
+ if let Some(cookie) = clear_cookie105
+ && let Ok(value) = cookie.parse()106
+ {107
+ headers.insert(header::SET_COOKIE, value);108
+ }109
+ (StatusCode::NO_CONTENT, headers).into_response()110
+}@@ -0,0 +1,73 @@
1
+//! Idle-exit tracking for the socket-activated static-only server: counts2
+//! in-flight requests and the time of the last one, so the process can shut3
+//! itself down once nothing has happened for `MALICE_IDLE_EXIT_SECS`.4
+5
+use std::{6
+ sync::{7
+ Arc,8
+ atomic::{AtomicUsize, Ordering},9
+ },10
+ time::{Duration, Instant},11
+};12
+13
+use axum::{extract::State, middleware::Next};14
+15
+#[derive(Clone)]16
+pub struct IdleTracker {17
+ in_flight: Arc<AtomicUsize>,18
+ last_activity: Arc<std::sync::Mutex<Instant>>,19
+}20
+21
+impl IdleTracker {22
+ pub fn new() -> Self {23
+ Self {24
+ in_flight: Arc::new(AtomicUsize::new(0)),25
+ last_activity: Arc::new(std::sync::Mutex::new(Instant::now())),26
+ }27
+ }28
+29
+ fn touch(&self) {30
+ *self31
+ .last_activity32
+ .lock()33
+ .expect("idle tracker mutex poisoned") = Instant::now();34
+ }35
+36
+ /// Resolves once no request has started or finished, and none is still37
+ /// in flight, for at least `idle`.38
+ pub async fn wait_idle(&self, idle: Duration) {39
+ loop {40
+ tokio::time::sleep(idle.min(Duration::from_millis(200))).await;41
+ if self.in_flight.load(Ordering::SeqCst) != 0 {42
+ continue;43
+ }44
+ let elapsed = self45
+ .last_activity46
+ .lock()47
+ .expect("idle tracker mutex poisoned")48
+ .elapsed();49
+ if elapsed >= idle {50
+ return;51
+ }52
+ }53
+ }54
+}55
+56
+impl Default for IdleTracker {57
+ fn default() -> Self {58
+ Self::new()59
+ }60
+}61
+62
+pub async fn track(63
+ State(idle): State<IdleTracker>,64
+ request: axum::extract::Request,65
+ next: Next,66
+) -> axum::response::Response {67
+ idle.in_flight.fetch_add(1, Ordering::SeqCst);68
+ idle.touch();69
+ let response = next.run(request).await;70
+ idle.touch();71
+ idle.in_flight.fetch_sub(1, Ordering::SeqCst);72
+ response73
+}@@ -0,0 +1,5 @@
1
+pub mod csrf;2
+pub mod error;3
+pub mod idle;4
+pub mod routes;5
+pub mod state;@@ -0,0 +1,159 @@
1
+use std::{future::Future, net::SocketAddr, path::PathBuf, time::Duration};2
+3
+use axum::{Router, middleware};4
+use listenfd::ListenFd;5
+use malice::{6
+ idle::IdleTracker,7
+ routes::{router, static_router},8
+ state::{AppState, ensure_dirs, load_or_create_master_key},9
+};10
+11
+fn env_flag(name: &str, default: bool) -> bool {12
+ match std::env::var(name) {13
+ Ok(value) => {14
+ let value = value.trim().to_ascii_lowercase();15
+ match value.as_str() {16
+ "1" | "true" | "yes" | "on" => true,17
+ "0" | "false" | "no" | "off" => false,18
+ _ => default,19
+ }20
+ }21
+ Err(_) => default,22
+ }23
+}24
+25
+fn data_dir() -> PathBuf {26
+ std::env::var("MALICE_DATA_DIR")27
+ .map(PathBuf::from)28
+ .unwrap_or_else(|_| PathBuf::from("local-data"))29
+}30
+31
+fn bind_addr() -> SocketAddr {32
+ std::env::var("MALICE_BIND")33
+ .unwrap_or_else(|_| "127.0.0.1:3000".to_owned())34
+ .parse()35
+ .expect("MALICE_BIND must be host:port")36
+}37
+38
+fn idle_exit_secs() -> Option<u64> {39
+ std::env::var("MALICE_IDLE_EXIT_SECS")40
+ .ok()41
+ .and_then(|value| value.trim().parse().ok())42
+}43
+44
+fn web_dir() -> Option<String> {45
+ if let Ok(dir) = std::env::var("MALICE_WEB_DIR") {46
+ assert!(47
+ std::path::Path::new(&dir).join("index.html").is_file(),48
+ "MALICE_WEB_DIR must point to built frontend assets containing index.html"49
+ );50
+ return Some(dir);51
+ }52
+53
+ let local = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("web/dist");54
+ local55
+ .join("index.html")56
+ .is_file()57
+ .then(|| local.to_string_lossy().into_owned())58
+}59
+60
+/// Takes over a systemd-activated listener from fd 3 when present, so the61
+/// socket unit can hold the port before the process ever starts. Falls back62
+/// to a plain bind otherwise.63
+async fn bind_listener() -> tokio::net::TcpListener {64
+ let mut listenfd = ListenFd::from_env();65
+ if let Some(listener) = listenfd66
+ .take_tcp_listener(0)67
+ .expect("inspecting inherited sockets")68
+ {69
+ listener70
+ .set_nonblocking(true)71
+ .expect("setting inherited socket nonblocking");72
+ return tokio::net::TcpListener::from_std(listener)73
+ .expect("adopting inherited socket into tokio");74
+ }75
+ let addr = bind_addr();76
+ tracing::info!("listening on {addr}");77
+ tokio::net::TcpListener::bind(addr)78
+ .await79
+ .expect("binding the listener must succeed")80
+}81
+82
+/// Serves `app` on `listener`, exiting on ctrl-c or, when83
+/// `MALICE_IDLE_EXIT_SECS` is set, after that long with no request in flight84
+/// or started. `on_shutdown` runs after the exit signal either way.85
+async fn serve(86
+ listener: tokio::net::TcpListener,87
+ app: Router,88
+ on_shutdown: impl Future<Output = ()> + Send + 'static,89
+) {90
+ let idle = idle_exit_secs().map(|secs| (IdleTracker::new(), secs));91
+ let app = match &idle {92
+ Some((tracker, _)) => app.layer(middleware::from_fn_with_state(93
+ tracker.clone(),94
+ malice::idle::track,95
+ )),96
+ None => app,97
+ };98
+ axum::serve(listener, app)99
+ .with_graceful_shutdown(async move {100
+ match idle {101
+ Some((tracker, secs)) => {102
+ tokio::select! {103
+ _ = tokio::signal::ctrl_c() => {}104
+ _ = tracker.wait_idle(Duration::from_secs(secs)) => {105
+ tracing::info!("idle for {secs}s with no requests, exiting");106
+ }107
+ }108
+ }109
+ None => {110
+ let _ = tokio::signal::ctrl_c().await;111
+ }112
+ }113
+ on_shutdown.await;114
+ })115
+ .await116
+ .expect("serving must succeed");117
+}118
+119
+async fn run_hosted() {120
+ let data_dir = data_dir();121
+ ensure_dirs(&data_dir);122
+ let master_key =123
+ load_or_create_master_key(&data_dir).expect("loading the master key must succeed");124
+ let cookie_secure = env_flag("MALICE_SESSION_COOKIE_SECURE", true);125
+ let allow_http_homeserver = env_flag("MALICE_ALLOW_HTTP_HOMESERVER", false);126
+ let state = AppState::new(data_dir, master_key, cookie_secure, allow_http_homeserver);127
+ let app = router(state.clone(), web_dir());128
+ let listener = bind_listener().await;129
+ serve(130
+ listener,131
+ app,132
+ async move { state.shutdown_gracefully().await },133
+ )134
+ .await;135
+}136
+137
+async fn run_static_only() {138
+ let app = static_router(web_dir());139
+ let listener = bind_listener().await;140
+ serve(listener, app, async {}).await;141
+}142
+143
+#[tokio::main]144
+async fn main() {145
+ tracing_subscriber::fmt()146
+ .with_env_filter(147
+ tracing_subscriber::EnvFilter::try_from_default_env()148
+ .unwrap_or_else(|_| "malice=info,tower_http=info".into()),149
+ )150
+ .with_target(false)151
+ .compact()152
+ .init();153
+154
+ if env_flag("MALICE_STATIC_ONLY", false) {155
+ run_static_only().await;156
+ } else {157
+ run_hosted().await;158
+ }159
+}@@ -0,0 +1,835 @@
1
+use std::sync::Arc;2
+3
+use axum::{4
+ Router,5
+ extract::{DefaultBodyLimit, Multipart, Query, State},6
+ http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header},7
+ middleware,8
+ middleware::Next,9
+ response::{IntoResponse, Response, Sse, sse::Event as SseEvent},10
+ routing::{any, get, post},11
+};12
+use futures_util::StreamExt;13
+use macltok::Retain;14
+use malice_core::{15
+ feed::subscribe,16
+ login::{LoginInput, sanitise_auth_error},17
+ request::{Reply, Request, dispatch},18
+};19
+use serde::{Deserialize, Serialize};20
+use tower_http::{21
+ services::{ServeDir, ServeFile},22
+ trace::TraceLayer,23
+};24
+25
+use crate::{26
+ csrf::{csrf_guard, session_sid, set_cookie_value},27
+ error::{ApiError, json_ok, no_content},28
+ state::{AppState, RestoreError, UserSession},29
+};30
+31
+#[derive(Debug, Deserialize)]32
+struct RoomIdBody {33
+ #[serde(default)]34
+ room_id: String,35
+}36
+37
+#[derive(Debug, Deserialize)]38
+struct SendBody {39
+ #[serde(default)]40
+ room_id: String,41
+ #[serde(default)]42
+ body: String,43
+}44
+45
+#[derive(Debug, Deserialize)]46
+struct PaginateBody {47
+ #[serde(default)]48
+ room_id: String,49
+}50
+51
+#[derive(Debug, Deserialize)]52
+struct JoinBody {53
+ #[serde(default)]54
+ room: String,55
+}56
+57
+#[derive(Debug, Deserialize)]58
+struct CreateBody {59
+ #[serde(default)]60
+ name: String,61
+ #[serde(default)]62
+ topic: Option<String>,63
+}64
+65
+#[derive(Debug, Deserialize)]66
+struct ReactBody {67
+ #[serde(default)]68
+ room_id: String,69
+ #[serde(default)]70
+ event_id: String,71
+ #[serde(default)]72
+ key: String,73
+}74
+75
+#[derive(Debug, Deserialize)]76
+struct ReadBody {77
+ #[serde(default)]78
+ room_id: String,79
+ #[serde(default)]80
+ event_id: String,81
+}82
+83
+#[derive(Debug, Deserialize)]84
+struct MembersQuery {85
+ #[serde(default)]86
+ room_id: String,87
+}88
+#[derive(Debug, Deserialize)]89
+struct RecoverBody {90
+ recovery_key: String,91
+}92
+93
+#[derive(Debug, Deserialize)]94
+struct VerifyBody {95
+ action: String,96
+}97
+98
+#[derive(Debug, Deserialize)]99
+struct MediaQuery {100
+ mxc: String,101
+}102
+#[derive(Debug, Deserialize)]103
+struct MessageMediaQuery {104
+ media_id: String,105
+ size: String,106
+}107
+108
+#[derive(Debug, Deserialize)]109
+struct TypingBody {110
+ #[serde(default)]111
+ room_id: String,112
+ #[serde(default)]113
+ typing: bool,114
+}115
+116
+#[derive(Debug, Serialize)]117
+struct SoftLogoutInfo {118
+ user_id: String,119
+ homeserver: String,120
+}121
+122
+#[derive(Debug, Serialize)]123
+struct SessionResponse {124
+ user_id: Option<String>,125
+ #[serde(skip_serializing_if = "Option::is_none")]126
+ soft_logout: Option<SoftLogoutInfo>,127
+}128
+129
+async fn auth(130
+ state: &AppState,131
+ headers: &HeaderMap,132
+) -> Result<(Arc<UserSession>, String), ApiError> {133
+ let Some(sid) = session_sid(headers) else {134
+ return Err(ApiError::unauthorized("not signed in"));135
+ };136
+ match state.get_or_restore(&sid).await {137
+ Ok(user) => Ok((user, sid)),138
+ Err(RestoreError::NotFound) => Err(ApiError::unauthorized_stale(139
+ "session expired; sign in again",140
+ state.cookie_secure(),141
+ )),142
+ Err(RestoreError::SoftLoggedOut { .. }) => Err(ApiError::unauthorized(143
+ "signed out by the server, sign in again",144
+ )),145
+ Err(RestoreError::Stale(message)) => {146
+ state.remove_session(&sid, Retain::Nothing).await;147
+ Err(ApiError::unauthorized_stale(message, state.cookie_secure()))148
+ }149
+ Err(RestoreError::Unavailable(message)) => {150
+ Err(ApiError::new(StatusCode::BAD_GATEWAY, message))151
+ }152
+ }153
+}154
+155
+/// commands answer when they're done, so the browser gets the outcome in the response156
+fn respond(result: Result<Reply, malice_core::error::CoreError>) -> Response {157
+ match result {158
+ Ok(Reply::Done) => no_content(None),159
+ Ok(Reply::Json(value)) => json_ok(&value, None),160
+ Ok(Reply::Avatar {161
+ bytes,162
+ content_type,163
+ }) => (164
+ [165
+ (axum::http::header::CONTENT_TYPE, content_type),166
+ (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),167
+ (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),168
+ ],169
+ bytes,170
+ )171
+ .into_response(),172
+ Ok(Reply::Attachment {173
+ bytes,174
+ content_type,175
+ }) => (176
+ [177
+ (178
+ axum::http::header::CONTENT_TYPE,179
+ content_type.unwrap_or("application/octet-stream"),180
+ ),181
+ (182
+ axum::http::header::CONTENT_DISPOSITION,183
+ if content_type.is_some() {184
+ "inline"185
+ } else {186
+ "attachment"187
+ },188
+ ),189
+ (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),190
+ (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),191
+ (axum::http::header::CONTENT_SECURITY_POLICY, "sandbox"),192
+ ],193
+ bytes,194
+ )195
+ .into_response(),196
+ Err(error) => ApiError::from(error).into_response(),197
+ }198
+}199
+200
+async fn get_session(State(state): State<AppState>, headers: HeaderMap) -> Response {201
+ let Some(sid) = session_sid(&headers) else {202
+ return json_ok(203
+ &SessionResponse {204
+ user_id: None,205
+ soft_logout: None,206
+ },207
+ None,208
+ );209
+ };210
+ match state.get_or_restore(&sid).await {211
+ Ok(user) => json_ok(212
+ &SessionResponse {213
+ user_id: Some(user.user_id.clone()),214
+ soft_logout: None,215
+ },216
+ None,217
+ ),218
+ Err(RestoreError::NotFound) => json_ok(219
+ &SessionResponse {220
+ user_id: None,221
+ soft_logout: None,222
+ },223
+ None,224
+ ),225
+ Err(RestoreError::SoftLoggedOut {226
+ user_id,227
+ homeserver,228
+ }) => json_ok(229
+ &SessionResponse {230
+ user_id: None,231
+ soft_logout: Some(SoftLogoutInfo {232
+ user_id,233
+ homeserver,234
+ }),235
+ },236
+ None,237
+ ),238
+ Err(RestoreError::Stale(message)) => {239
+ state.remove_session(&sid, Retain::Nothing).await;240
+ ApiError::unauthorized_stale(message, state.cookie_secure()).into_response()241
+ }242
+ Err(RestoreError::Unavailable(message)) => {243
+ ApiError::new(StatusCode::BAD_GATEWAY, message).into_response()244
+ }245
+ }246
+}247
+248
+async fn post_login(249
+ State(state): State<AppState>,250
+ headers: HeaderMap,251
+ body: axum::Json<LoginInput>,252
+) -> Response {253
+ let input = match body.0.validate(state.allow_http_homeserver()) {254
+ Ok(input) => input,255
+ Err(error) => return ApiError::from(error).into_response(),256
+ };257
+ let cookie_sid = session_sid(&headers);258
+ let password = input.password.clone();259
+ match state260
+ .login(261
+ cookie_sid.as_deref(),262
+ input.homeserver,263
+ input.username,264
+ input.password,265
+ )266
+ .await267
+ {268
+ Ok(user) => json_ok(269
+ &serde_json::json!({ "user_id": user.user_id }),270
+ Some(set_cookie_value(&user.sid, state.cookie_secure())),271
+ ),272
+ Err(message) => {273
+ let status = if message.contains("starting sync engine")274
+ || message.contains("could not save the session")275
+ || message.contains("another engine is using this session")276
+ {277
+ StatusCode::BAD_GATEWAY278
+ } else {279
+ StatusCode::UNAUTHORIZED280
+ };281
+ ApiError::new(status, sanitise_auth_error(&message, &password)).into_response()282
+ }283
+ }284
+}285
+286
+async fn post_logout(State(state): State<AppState>, headers: HeaderMap) -> Response {287
+ if let Some(sid) = session_sid(&headers) {288
+ state.logout(&sid).await;289
+ }290
+ no_content(Some(crate::csrf::clear_cookie_value(state.cookie_secure())))291
+}292
+293
+async fn get_rooms(State(state): State<AppState>, headers: HeaderMap) -> Response {294
+ let (user, _) = match auth(&state, &headers).await {295
+ Ok(pair) => pair,296
+ Err(error) => return error.into_response(),297
+ };298
+ respond(dispatch(&user.handle, &user.mirror, Request::Rooms).await)299
+}300
+301
+async fn get_members(302
+ State(state): State<AppState>,303
+ headers: HeaderMap,304
+ Query(query): Query<MembersQuery>,305
+) -> Response {306
+ let (user, _) = match auth(&state, &headers).await {307
+ Ok(pair) => pair,308
+ Err(error) => return error.into_response(),309
+ };310
+ respond(311
+ dispatch(312
+ &user.handle,313
+ &user.mirror,314
+ Request::Members {315
+ room_id: query.room_id,316
+ },317
+ )318
+ .await,319
+ )320
+}321
+322
+async fn rooms_open(323
+ State(state): State<AppState>,324
+ headers: HeaderMap,325
+ body: axum::Json<RoomIdBody>,326
+) -> Response {327
+ let (user, _) = match auth(&state, &headers).await {328
+ Ok(pair) => pair,329
+ Err(error) => return error.into_response(),330
+ };331
+ respond(332
+ dispatch(333
+ &user.handle,334
+ &user.mirror,335
+ Request::Open {336
+ room_id: body.0.room_id,337
+ },338
+ )339
+ .await,340
+ )341
+}342
+343
+async fn rooms_close(344
+ State(state): State<AppState>,345
+ headers: HeaderMap,346
+ body: axum::Json<RoomIdBody>,347
+) -> Response {348
+ let (user, _) = match auth(&state, &headers).await {349
+ Ok(pair) => pair,350
+ Err(error) => return error.into_response(),351
+ };352
+ respond(353
+ dispatch(354
+ &user.handle,355
+ &user.mirror,356
+ Request::Close {357
+ room_id: body.0.room_id,358
+ },359
+ )360
+ .await,361
+ )362
+}363
+364
+async fn rooms_send(365
+ State(state): State<AppState>,366
+ headers: HeaderMap,367
+ body: axum::Json<SendBody>,368
+) -> Response {369
+ let (user, _) = match auth(&state, &headers).await {370
+ Ok(pair) => pair,371
+ Err(error) => return error.into_response(),372
+ };373
+ respond(374
+ dispatch(375
+ &user.handle,376
+ &user.mirror,377
+ Request::Send {378
+ room_id: body.0.room_id,379
+ body: body.0.body,380
+ },381
+ )382
+ .await,383
+ )384
+}385
+386
+async fn rooms_upload(387
+ State(state): State<AppState>,388
+ headers: HeaderMap,389
+ mut upload: Multipart,390
+) -> Response {391
+ let (user, _) = match auth(&state, &headers).await {392
+ Ok(pair) => pair,393
+ Err(error) => return error.into_response(),394
+ };395
+ let mut room_id = None;396
+ let mut file = None;397
+ loop {398
+ let field = match upload.next_field().await {399
+ Ok(Some(field)) => field,400
+ Ok(None) => break,401
+ Err(_) => return ApiError::bad_request("invalid upload form").into_response(),402
+ };403
+ match field.name() {404
+ Some("room_id") if room_id.is_none() => {405
+ room_id = match field.text().await {406
+ Ok(value) => Some(value),407
+ Err(_) => return ApiError::bad_request("invalid room_id").into_response(),408
+ };409
+ }410
+ Some("file") if file.is_none() => {411
+ let filename = field.file_name().unwrap_or("attachment").to_owned();412
+ let mimetype = field413
+ .content_type()414
+ .unwrap_or("application/octet-stream")415
+ .to_owned();416
+ let bytes = match field.bytes().await {417
+ Ok(bytes) => bytes,418
+ Err(_) => return ApiError::bad_request("invalid file").into_response(),419
+ };420
+ if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {421
+ return ApiError::bad_request("file must be between 1 byte and 20 MiB")422
+ .into_response();423
+ }424
+ file = Some((filename, mimetype, bytes));425
+ }426
+ _ => return ApiError::bad_request("unexpected upload field").into_response(),427
+ }428
+ }429
+ let Some(room_id) = room_id else {430
+ return ApiError::bad_request("room_id is required").into_response();431
+ };432
+ let Some((filename, mimetype, bytes)) = file else {433
+ return ApiError::bad_request("file is required").into_response();434
+ };435
+ respond(436
+ dispatch(437
+ &user.handle,438
+ &user.mirror,439
+ Request::upload(room_id, filename, mimetype, bytes.to_vec()),440
+ )441
+ .await,442
+ )443
+}444
+445
+async fn rooms_paginate(446
+ State(state): State<AppState>,447
+ headers: HeaderMap,448
+ body: axum::Json<PaginateBody>,449
+) -> Response {450
+ let (user, _) = match auth(&state, &headers).await {451
+ Ok(pair) => pair,452
+ Err(error) => return error.into_response(),453
+ };454
+ respond(455
+ dispatch(456
+ &user.handle,457
+ &user.mirror,458
+ Request::Paginate {459
+ room_id: body.0.room_id,460
+ },461
+ )462
+ .await,463
+ )464
+}465
+466
+async fn rooms_join(467
+ State(state): State<AppState>,468
+ headers: HeaderMap,469
+ body: axum::Json<JoinBody>,470
+) -> Response {471
+ let (user, _) = match auth(&state, &headers).await {472
+ Ok(pair) => pair,473
+ Err(error) => return error.into_response(),474
+ };475
+ respond(476
+ dispatch(477
+ &user.handle,478
+ &user.mirror,479
+ Request::Join { room: body.0.room },480
+ )481
+ .await,482
+ )483
+}484
+485
+async fn rooms_create(486
+ State(state): State<AppState>,487
+ headers: HeaderMap,488
+ body: axum::Json<CreateBody>,489
+) -> Response {490
+ let (user, _) = match auth(&state, &headers).await {491
+ Ok(pair) => pair,492
+ Err(error) => return error.into_response(),493
+ };494
+ respond(495
+ dispatch(496
+ &user.handle,497
+ &user.mirror,498
+ Request::Create {499
+ name: body.0.name,500
+ topic: body.0.topic,501
+ },502
+ )503
+ .await,504
+ )505
+}506
+507
+async fn rooms_leave(508
+ State(state): State<AppState>,509
+ headers: HeaderMap,510
+ body: axum::Json<RoomIdBody>,511
+) -> Response {512
+ let (user, _) = match auth(&state, &headers).await {513
+ Ok(pair) => pair,514
+ Err(error) => return error.into_response(),515
+ };516
+ respond(517
+ dispatch(518
+ &user.handle,519
+ &user.mirror,520
+ Request::Leave {521
+ room_id: body.0.room_id,522
+ },523
+ )524
+ .await,525
+ )526
+}527
+528
+async fn rooms_react(529
+ State(state): State<AppState>,530
+ headers: HeaderMap,531
+ body: axum::Json<ReactBody>,532
+) -> Response {533
+ let (user, _) = match auth(&state, &headers).await {534
+ Ok(pair) => pair,535
+ Err(error) => return error.into_response(),536
+ };537
+ respond(538
+ dispatch(539
+ &user.handle,540
+ &user.mirror,541
+ Request::React {542
+ room_id: body.0.room_id,543
+ event_id: body.0.event_id,544
+ key: body.0.key,545
+ },546
+ )547
+ .await,548
+ )549
+}550
+551
+async fn rooms_read(552
+ State(state): State<AppState>,553
+ headers: HeaderMap,554
+ body: axum::Json<ReadBody>,555
+) -> Response {556
+ let (user, _) = match auth(&state, &headers).await {557
+ Ok(pair) => pair,558
+ Err(error) => return error.into_response(),559
+ };560
+ respond(561
+ dispatch(562
+ &user.handle,563
+ &user.mirror,564
+ Request::Read {565
+ room_id: body.0.room_id,566
+ event_id: body.0.event_id,567
+ },568
+ )569
+ .await,570
+ )571
+}572
+573
+async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {574
+ let (user, _) = match auth(&state, &headers).await {575
+ Ok(pair) => pair,576
+ Err(error) => return error.into_response(),577
+ };578
+ respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)579
+}580
+581
+async fn get_space_children(582
+ State(state): State<AppState>,583
+ headers: HeaderMap,584
+ Query(query): Query<MembersQuery>,585
+) -> Response {586
+ let (user, _) = match auth(&state, &headers).await {587
+ Ok(pair) => pair,588
+ Err(error) => return error.into_response(),589
+ };590
+ respond(591
+ dispatch(592
+ &user.handle,593
+ &user.mirror,594
+ Request::SpaceChildren {595
+ room_id: query.room_id,596
+ },597
+ )598
+ .await,599
+ )600
+}601
+602
+async fn get_media(603
+ State(state): State<AppState>,604
+ headers: HeaderMap,605
+ Query(query): Query<MediaQuery>,606
+) -> Response {607
+ let (user, _) = match auth(&state, &headers).await {608
+ Ok(pair) => pair,609
+ Err(error) => return error.into_response(),610
+ };611
+ respond(612
+ dispatch(613
+ &user.handle,614
+ &user.mirror,615
+ Request::Avatar { mxc: query.mxc },616
+ )617
+ .await,618
+ )619
+}620
+621
+async fn get_message_media(622
+ State(state): State<AppState>,623
+ headers: HeaderMap,624
+ Query(query): Query<MessageMediaQuery>,625
+) -> Response {626
+ let (user, _) = match auth(&state, &headers).await {627
+ Ok(pair) => pair,628
+ Err(error) => return error.into_response(),629
+ };630
+ // the declared mimetype is the sender's claim, so the bytes get sniffed instead631
+ respond(632
+ dispatch(633
+ &user.handle,634
+ &user.mirror,635
+ Request::Media {636
+ media_id: query.media_id,637
+ size: query.size,638
+ },639
+ )640
+ .await,641
+ )642
+}643
+644
+async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {645
+ let (user, _) = match auth(&state, &headers).await {646
+ Ok(pair) => pair,647
+ Err(error) => return error.into_response(),648
+ };649
+ respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)650
+}651
+652
+async fn recover_encryption(653
+ State(state): State<AppState>,654
+ headers: HeaderMap,655
+ body: axum::Json<RecoverBody>,656
+) -> Response {657
+ let (user, _) = match auth(&state, &headers).await {658
+ Ok(pair) => pair,659
+ Err(error) => return error.into_response(),660
+ };661
+ respond(662
+ dispatch(663
+ &user.handle,664
+ &user.mirror,665
+ Request::Recover {666
+ recovery_key: body.0.recovery_key,667
+ },668
+ )669
+ .await,670
+ )671
+}672
+673
+async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {674
+ let (user, _) = match auth(&state, &headers).await {675
+ Ok(pair) => pair,676
+ Err(error) => return error.into_response(),677
+ };678
+ respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)679
+}680
+681
+async fn verify_encryption(682
+ State(state): State<AppState>,683
+ headers: HeaderMap,684
+ body: axum::Json<VerifyBody>,685
+) -> Response {686
+ let (user, _) = match auth(&state, &headers).await {687
+ Ok(pair) => pair,688
+ Err(error) => return error.into_response(),689
+ };690
+ respond(691
+ dispatch(692
+ &user.handle,693
+ &user.mirror,694
+ Request::Verify {695
+ action: body.0.action,696
+ },697
+ )698
+ .await,699
+ )700
+}701
+702
+async fn rooms_typing(703
+ State(state): State<AppState>,704
+ headers: HeaderMap,705
+ body: axum::Json<TypingBody>,706
+) -> Response {707
+ let (user, _) = match auth(&state, &headers).await {708
+ Ok(pair) => pair,709
+ Err(error) => return error.into_response(),710
+ };711
+ respond(712
+ dispatch(713
+ &user.handle,714
+ &user.mirror,715
+ Request::Typing {716
+ room_id: body.0.room_id,717
+ typing: body.0.typing,718
+ },719
+ )720
+ .await,721
+ )722
+}723
+724
+async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {725
+ let (user, _) = match auth(&state, &headers).await {726
+ Ok(pair) => pair,727
+ Err(error) => return error.into_response(),728
+ };729
+ let body = subscribe(&user.mirror).map(|event| {730
+ let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());731
+ Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))732
+ });733
+ Sse::new(body).into_response()734
+}735
+736
+async fn get_health() -> Response {737
+ json_ok(&serde_json::json!({ "status": "ok" }), None)738
+}739
+740
+async fn static_shell() -> Response {741
+ (742
+ StatusCode::SERVICE_UNAVAILABLE,743
+ axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),744
+ )745
+ .into_response()746
+}747
+748
+pub fn router(state: AppState, web_dir: Option<String>) -> Router {749
+ let api = Router::new()750
+ .route("/session", get(get_session))751
+ .route("/login", post(post_login))752
+ .route("/logout", post(post_logout))753
+ .route("/rooms", get(get_rooms))754
+ .route("/events", get(get_events))755
+ .route("/health", get(get_health))756
+ .route("/rooms/open", post(rooms_open))757
+ .route("/rooms/close", post(rooms_close))758
+ .route("/rooms/send", post(rooms_send))759
+ .route("/rooms/paginate", post(rooms_paginate))760
+ .route(761
+ "/rooms/upload",762
+ post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),763
+ )764
+ .route("/rooms/join", post(rooms_join))765
+ .route("/rooms/create", post(rooms_create))766
+ .route("/rooms/leave", post(rooms_leave))767
+ .route("/rooms/react", post(rooms_react))768
+ .route("/rooms/read", post(rooms_read))769
+ .route("/rooms/typing", post(rooms_typing))770
+ .route("/profile", get(get_profile))771
+ .route("/media", get(get_media))772
+ .route("/message-media", get(get_message_media))773
+ .route("/spaces/children", get(get_space_children))774
+ .route("/rooms/members", get(get_members))775
+ .route("/encryption", get(get_encryption))776
+ .route("/encryption/recover", post(recover_encryption))777
+ .route("/encryption/start", post(start_verification))778
+ .route("/encryption/verify", post(verify_encryption));779
+ let api = Router::new()780
+ .nest("/api", api)781
+ .layer(middleware::from_fn(csrf_guard));782
+ let router = api.with_state(state).layer(TraceLayer::new_for_http());783
+ match web_dir {784
+ Some(dir) => {785
+ let files =786
+ ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));787
+ router.fallback_service(files)788
+ }789
+ None => router.fallback(static_shell),790
+ }791
+}792
+793
+async fn api_not_found() -> StatusCode {794
+ StatusCode::NOT_FOUND795
+}796
+797
+fn is_no_cache_path(path: &str) -> bool {798
+ matches!(path, "/" | "/index.html" | "/sw.js")799
+}800
+801
+/// index.html and sw.js must never be cached, or a stale service worker or802
+/// app shell survives a rebuild; the rest of the bundle is hashed and can be803
+/// cached freely.804
+async fn static_headers(request: axum::extract::Request, next: Next) -> Response {805
+ let no_cache = is_no_cache_path(request.uri().path());806
+ let mut response = next.run(request).await;807
+ if no_cache {808
+ let headers = response.headers_mut();809
+ headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));810
+ headers.insert(811
+ HeaderName::from_static("service-worker-allowed"),812
+ HeaderValue::from_static("/"),813
+ );814
+ }815
+ response816
+}817
+818
+/// Serves only the built frontend: no `/api` routes, no session state, no819
+/// data dir. Used for the socket-activated on-device build.820
+pub fn static_router(web_dir: Option<String>) -> Router {821
+ let router = Router::new()822
+ .route("/api", any(api_not_found))823
+ .route("/api/{*rest}", any(api_not_found))824
+ .layer(TraceLayer::new_for_http());825
+ match web_dir {826
+ Some(dir) => {827
+ let files =828
+ ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));829
+ router830
+ .fallback_service(files)831
+ .layer(middleware::from_fn(static_headers))832
+ }833
+ None => router.fallback(static_shell),834
+ }835
+}@@ -0,0 +1,497 @@
1
+use std::{2
+ collections::HashMap,3
+ path::{Path, PathBuf},4
+ sync::Arc,5
+ time::Duration,6
+};7
+8
+use futures_util::StreamExt;9
+use macltok::{10
+ AccountError, ClientHandle, EncryptedSessionStore, EngineConfig, EngineError, Mirror,11
+ MirrorHandle, PasswordLogin, Restore, Retain, Session, SessionStatus, SessionStore, SoftLogout,12
+ StateChange, StoreConfig, Termination,13
+};14
+use rand_core::{OsRng, RngCore};15
+use sha2::{Digest, Sha256};16
+use tokio::{sync::Mutex, task::JoinHandle, time::timeout};17
+18
+use crate::csrf::valid_sid;19
+20
+const AUTH_TIMEOUT: Duration = Duration::from_secs(20);21
+22
+pub struct UserSession {23
+ pub sid: String,24
+ pub user_id: String,25
+ pub handle: ClientHandle,26
+ pub mirror: MirrorHandle,27
+ sessions: Arc<dyn SessionStore>,28
+ engine: Mutex<Option<JoinHandle<Result<Termination, EngineError>>>>,29
+ mirror_task: Mutex<Option<JoinHandle<()>>>,30
+}31
+32
+struct Inner {33
+ sessions: tokio::sync::RwLock<HashMap<String, Arc<UserSession>>>,34
+ /// Serialises concurrent restores of the same sid so two engines never35
+ /// open the same SQLite store at once.36
+ init_lock: Mutex<()>,37
+ data_dir: PathBuf,38
+ master_key: [u8; 32],39
+ cookie_secure: bool,40
+ allow_http_homeserver: bool,41
+}42
+43
+#[derive(Clone)]44
+pub struct AppState {45
+ inner: Arc<Inner>,46
+}47
+48
+#[derive(Debug)]49
+pub enum RestoreError {50
+ /// No cookie session and nothing on disk: plain guest.51
+ NotFound,52
+ /// Credential on disk is unusable: caller should clear the cookie.53
+ Stale(String),54
+ /// Credential looks fine but the engine would not start.55
+ Unavailable(String),56
+ /// The server logged this device out; it can log back in as the same user.57
+ SoftLoggedOut { user_id: String, homeserver: String },58
+}59
+60
+impl AppState {61
+ pub fn new(62
+ data_dir: PathBuf,63
+ master_key: [u8; 32],64
+ cookie_secure: bool,65
+ allow_http_homeserver: bool,66
+ ) -> Self {67
+ Self {68
+ inner: Arc::new(Inner {69
+ sessions: tokio::sync::RwLock::new(HashMap::new()),70
+ init_lock: Mutex::new(()),71
+ data_dir,72
+ master_key,73
+ cookie_secure,74
+ allow_http_homeserver,75
+ }),76
+ }77
+ }78
+79
+ pub fn allow_http_homeserver(&self) -> bool {80
+ self.inner.allow_http_homeserver81
+ }82
+83
+ pub fn cookie_secure(&self) -> bool {84
+ self.inner.cookie_secure85
+ }86
+87
+ fn sessions_dir(&self) -> PathBuf {88
+ self.inner.data_dir.join("sessions")89
+ }90
+91
+ fn session_file(&self, sid: &str) -> PathBuf {92
+ self.sessions_dir().join(format!("{sid}.bin"))93
+ }94
+95
+ fn store_dir(&self, sid: &str) -> PathBuf {96
+ self.inner.data_dir.join("stores").join(sid)97
+ }98
+99
+ fn store_passphrase(&self, sid: &str) -> String {100
+ let mut hasher = Sha256::new();101
+ hasher.update(self.inner.master_key);102
+ hasher.update(b"malice-store-v1:");103
+ hasher.update(sid.as_bytes());104
+ hex::encode(hasher.finalize())105
+ }106
+107
+ fn store_config(&self, sid: &str) -> StoreConfig {108
+ StoreConfig::sqlite(self.store_dir(sid), self.store_passphrase(sid))109
+ }110
+111
+ /// A fresh handle onto a sid's record and store. Cheap: nothing touches112
+ /// disk until the handle is actually used.113
+ fn session_store(&self, sid: &str) -> Arc<dyn SessionStore> {114
+ Arc::new(EncryptedSessionStore::file(115
+ self.session_file(sid),116
+ self.store_config(sid),117
+ ))118
+ }119
+120
+ async fn apply_retention(&self, sessions: &Arc<dyn SessionStore>, retain: Retain) {121
+ let result = match retain {122
+ Retain::RecordAndStore => Ok(()),123
+ Retain::Store => sessions.soft_log_out().await,124
+ Retain::Nothing => sessions.discard().await,125
+ };126
+ if let Err(error) = result {127
+ tracing::warn!(%error, "applying session retention failed");128
+ }129
+ }130
+131
+ /// Resolve a cookie sid to a live session, restoring from disk on demand.132
+ /// The shared map is never held across network or engine work: the fast133
+ /// path clones an `Arc` under a read lock, and restores serialise on a134
+ /// short init lock with a double-checked insert.135
+ pub async fn get_or_restore(&self, sid: &str) -> Result<Arc<UserSession>, RestoreError> {136
+ if !valid_sid(sid) {137
+ return Err(RestoreError::NotFound);138
+ }139
+ if let Some(user) = self.inner.sessions.read().await.get(sid).cloned() {140
+ return Ok(user);141
+ }142
+ let _init = self.inner.init_lock.lock().await;143
+ if let Some(user) = self.inner.sessions.read().await.get(sid).cloned() {144
+ return Ok(user);145
+ }146
+ let sessions = self.session_store(sid);147
+ let restore = Session::restore(sessions.clone()).await.map_err(|error| {148
+ tracing::warn!("restoring persisted session failed: {error}");149
+ restore_error(error)150
+ })?;151
+ match restore {152
+ None => Err(RestoreError::NotFound),153
+ Some(Restore::SoftLoggedOut(soft)) => Err(RestoreError::SoftLoggedOut {154
+ user_id: soft.user_id.to_string(),155
+ homeserver: soft.homeserver,156
+ }),157
+ Some(Restore::Resumed(session)) => self158
+ .start_user_session(sid.to_owned(), session, sessions)159
+ .await160
+ .map_err(|error| {161
+ tracing::warn!("starting restored session failed: {error}");162
+ RestoreError::Unavailable(error)163
+ }),164
+ }165
+ }166
+167
+ /// Restore just far enough to tell whether `sid`'s record is a soft168
+ /// logout, without disturbing anything else.169
+ async fn soft_logged_out(&self, sid: &str) -> Option<SoftLogout> {170
+ match Session::restore(self.session_store(sid)).await {171
+ Ok(Some(Restore::SoftLoggedOut(soft))) => Some(soft),172
+ _ => None,173
+ }174
+ }175
+176
+ /// Password login: creates a fresh opaque sid and starts the engine,177
+ /// which saves the record itself. The password never leaves this call.178
+ /// When the cookie sid names the same user's soft-logged-out device, logs179
+ /// back in as that device against the same sid instead, keeping its keys.180
+ pub async fn login(181
+ &self,182
+ cookie_sid: Option<&str>,183
+ homeserver: String,184
+ username: String,185
+ password: String,186
+ ) -> Result<Arc<UserSession>, String> {187
+ if let Some(sid) = cookie_sid.filter(|sid| valid_sid(sid))188
+ && let Some(soft) = self.soft_logged_out(sid).await189
+ {190
+ if username_matches(&username, &soft.user_id) {191
+ let sessions = self.session_store(sid);192
+ let session = login_with(soft.password_login(password), sessions.clone()).await?;193
+ let _init = self.inner.init_lock.lock().await;194
+ return self195
+ .start_user_session(sid.to_owned(), session, sessions)196
+ .await;197
+ }198
+ self.apply_retention(&self.session_store(sid), Retain::Nothing)199
+ .await;200
+ }201
+ let sid = new_sid();202
+ let sessions = self.session_store(&sid);203
+ let session = login_with(204
+ PasswordLogin::new(homeserver, username, password)205
+ .with_device_display_name("malice web"),206
+ sessions.clone(),207
+ )208
+ .await?;209
+ let _init = self.inner.init_lock.lock().await;210
+ self.start_user_session(sid, session, sessions).await211
+ }212
+213
+ async fn start_user_session(214
+ &self,215
+ sid: String,216
+ session: Session,217
+ sessions: Arc<dyn SessionStore>,218
+ ) -> Result<Arc<UserSession>, String> {219
+ let (engine, handle, events) = session220
+ .build(EngineConfig::default())221
+ .await222
+ .map_err(|error| format!("starting sync engine: {error}"))?;223
+ let engine_task = tokio::spawn(engine.run());224
+ let (mirror, mirror_handle) = Mirror::new(handle.clone(), events);225
+ let mirror_task = tokio::spawn(mirror.run());226
+ let Some(user_id) = wait_authenticated(&mirror_handle).await else {227
+ engine_task.abort();228
+ let _ = engine_task.await;229
+ mirror_task.abort();230
+ let _ = mirror_task.await;231
+ return Err("sync engine stopped before authenticating".to_owned());232
+ };233
+ let user = Arc::new(UserSession {234
+ sid: sid.clone(),235
+ user_id,236
+ handle,237
+ mirror: mirror_handle,238
+ sessions,239
+ engine: Mutex::new(Some(engine_task)),240
+ mirror_task: Mutex::new(Some(mirror_task)),241
+ });242
+ {243
+ let mut sessions_map = self.inner.sessions.write().await;244
+ sessions_map.insert(sid, user.clone());245
+ }246
+ let supervisor_state = self.clone();247
+ let supervisor_user = user.clone();248
+ tokio::spawn(async move {249
+ supervise(supervisor_state, supervisor_user).await;250
+ });251
+ Ok(user)252
+ }253
+254
+ /// Logout: best-effort server-side logout, then drop all local state and255
+ /// delete persisted secrets. Never holds the map lock across the network256
+ /// call. Forces `Retain::Nothing` regardless of what the engine's own257
+ /// termination said, so an explicit logout always ends in a full discard258
+ /// even if it raced the supervisor over a server-initiated soft logout.259
+ pub async fn logout(&self, sid: &str) {260
+ let user = { self.inner.sessions.read().await.get(sid).cloned() };261
+ if let Some(user) = user {262
+ // answers once the server has let go of the device, and the engine stops itself263
+ let _ = timeout(Duration::from_secs(5), user.handle.logout()).await;264
+ }265
+ self.remove_session(sid, Retain::Nothing).await;266
+ }267
+268
+ /// Forget an in-memory session, abort its engine and mirror tasks, and269
+ /// apply the given retention to its persisted secrets. Called both for270
+ /// explicit logout (always `Retain::Nothing`) and from the supervisor271
+ /// once an engine's `Termination` says what its record and store are272
+ /// still worth. Retention always runs, even if the session was already273
+ /// gone from the map, so a racing logout's `Nothing` still lands.274
+ pub async fn remove_session(&self, sid: &str, retain: Retain) {275
+ // A restore must not reopen the store between removing the map entry276
+ // and deleting its persisted credentials.277
+ let _init = self.inner.init_lock.lock().await;278
+ let user = self.inner.sessions.write().await.remove(sid);279
+ let sessions = match &user {280
+ Some(user) => user.sessions.clone(),281
+ None => self.session_store(sid),282
+ };283
+ if let Some(user) = &user {284
+ if let Some(task) = user.engine.lock().await.take() {285
+ task.abort();286
+ }287
+ if let Some(task) = user.mirror_task.lock().await.take() {288
+ task.abort();289
+ }290
+ }291
+ self.apply_retention(&sessions, retain).await;292
+ }293
+294
+ /// Graceful shutdown: ask every engine to stop, keep persisted records so295
+ /// the next boot restores them.296
+ pub async fn shutdown_gracefully(&self) {297
+ let users: Vec<Arc<UserSession>> =298
+ self.inner.sessions.read().await.values().cloned().collect();299
+ for user in &users {300
+ // the supervisor keeps waiting, so a shutdown can answer once `Stopped` is out301
+ let handle = user.handle.clone();302
+ tokio::spawn(async move {303
+ let _ = timeout(Duration::from_secs(5), handle.shutdown()).await;304
+ });305
+ }306
+ // Give engines a moment to emit `Stopped`; the supervisor then drops307
+ // each in-memory session while leaving persisted records behind.308
+ timeout(Duration::from_secs(5), async {309
+ loop {310
+ if self.inner.sessions.read().await.is_empty() {311
+ break;312
+ }313
+ tokio::time::sleep(Duration::from_millis(50)).await;314
+ }315
+ })316
+ .await317
+ .ok();318
+ for user in &users {319
+ if let Some(task) = user.engine.lock().await.take() {320
+ task.abort();321
+ }322
+ if let Some(task) = user.mirror_task.lock().await.take() {323
+ task.abort();324
+ }325
+ }326
+ self.inner.sessions.write().await.clear();327
+ }328
+}329
+330
+fn restore_error(error: AccountError) -> RestoreError {331
+ match error {332
+ AccountError::StoreInUse => {333
+ RestoreError::Unavailable("another connection is using this session".to_owned())334
+ }335
+ error => RestoreError::Stale(format!("restoring persisted session failed: {error}")),336
+ }337
+}338
+339
+async fn login_with(340
+ login: PasswordLogin,341
+ sessions: Arc<dyn SessionStore>,342
+) -> Result<Session, String> {343
+ match Session::login_password(login, sessions).await {344
+ Ok(session) => Ok(session),345
+ Err(AccountError::StoreInUse) => Err("another engine is using this session".to_owned()),346
+ Err(AccountError::Unsaved { session, .. }) => session347
+ .save()348
+ .await349
+ .map_err(|_| "could not save the session".to_owned()),350
+ Err(error) => Err(format!(351
+ "login failed: {}",352
+ malice_core::login::describe_error(&error)353
+ )),354
+ }355
+}356
+357
+fn username_matches(submitted: &str, user_id: &ruma::OwnedUserId) -> bool {358
+ let submitted = submitted.trim();359
+ submitted == user_id.as_str() || submitted == user_id.localpart()360
+}361
+362
+/// Wait for the mirror to learn the user id. Fails once the subscription363
+/// ends or the session leaves `Active` before that happens.364
+async fn wait_authenticated(mirror: &MirrorHandle) -> Option<String> {365
+ timeout(AUTH_TIMEOUT, async {366
+ let mut updates = mirror.subscribe();367
+ loop {368
+ let update = updates.next().await?;369
+ match update.change {370
+ StateChange::State(state) => {371
+ if let Some(user_id) = &state.user_id {372
+ return Some(user_id.to_string());373
+ }374
+ if state.session != SessionStatus::Active {375
+ return None;376
+ }377
+ }378
+ StateChange::Authenticated(user_id) => return Some(user_id.to_string()),379
+ StateChange::Session(status) if status != SessionStatus::Active => return None,380
+ _ => {}381
+ }382
+ }383
+ })384
+ .await385
+ .ok()386
+ .flatten()387
+}388
+389
+/// Awaits a session's engine, then its mirror, removes it from the map, and390
+/// applies the retention its `Termination` earns. Taking the engine391
+/// `JoinHandle` is the single point two racing teardowns (this task and an392
+/// explicit logout) arbitrate over: whichever finds it empty does nothing393
+/// further, so the engine and mirror are never aborted twice.394
+async fn supervise(state: AppState, user: Arc<UserSession>) {395
+ let Some(engine_task) = user.engine.lock().await.take() else {396
+ return;397
+ };398
+ let retain = match engine_task.await {399
+ Ok(Ok(termination)) => termination.retain(),400
+ Ok(Err(error)) => {401
+ tracing::warn!(%error, "sync engine ended with an error; keeping its session");402
+ Retain::RecordAndStore403
+ }404
+ Err(error) => {405
+ tracing::warn!(%error, "sync engine task did not finish cleanly; keeping its session");406
+ Retain::RecordAndStore407
+ }408
+ };409
+ if let Some(mirror_task) = user.mirror_task.lock().await.take() {410
+ let _ = mirror_task.await;411
+ }412
+ state.remove_session(&user.sid, retain).await;413
+}414
+415
+fn new_sid() -> String {416
+ let mut bytes = [0u8; 32];417
+ OsRng.fill_bytes(&mut bytes);418
+ hex::encode(bytes)419
+}420
+421
+/// Load the 32-byte master key, creating it with owner-only permissions when422
+/// absent. Secrets must never be world-readable.423
+pub fn load_or_create_master_key(data_dir: &Path) -> Result<[u8; 32], String> {424
+ create_private_dir(data_dir).map_err(|error| format!("creating data dir: {error}"))?;425
+ restrict_dir(data_dir);426
+ let path = data_dir.join("master.key");427
+ if path.exists() {428
+ let bytes = std::fs::read(&path).map_err(|error| format!("reading master key: {error}"))?;429
+ restrict_file(&path);430
+ if bytes.len() != 32 {431
+ return Err("master key has an unexpected length".to_owned());432
+ }433
+ let mut key = [0u8; 32];434
+ key.copy_from_slice(&bytes);435
+ return Ok(key);436
+ }437
+ let mut key = [0u8; 32];438
+ OsRng.fill_bytes(&mut key);439
+ let mut file =440
+ create_private_file(&path).map_err(|error| format!("creating master key: {error}"))?;441
+ if let Err(error) = std::io::Write::write_all(&mut file, &key).and_then(|_| file.sync_all()) {442
+ let _ = std::fs::remove_file(&path);443
+ return Err(format!("writing master key: {error}"));444
+ }445
+ Ok(key)446
+}447
+448
+fn create_private_dir(path: &Path) -> std::io::Result<()> {449
+ let mut builder = std::fs::DirBuilder::new();450
+ builder.recursive(true);451
+ #[cfg(unix)]452
+ {453
+ use std::os::unix::fs::DirBuilderExt;454
+ builder.mode(0o700);455
+ }456
+ builder.create(path)457
+}458
+459
+fn create_private_file(path: &Path) -> std::io::Result<std::fs::File> {460
+ let mut options = std::fs::OpenOptions::new();461
+ options.write(true).create_new(true);462
+ #[cfg(unix)]463
+ {464
+ use std::os::unix::fs::OpenOptionsExt;465
+ options.mode(0o600);466
+ }467
+ options.open(path)468
+}469
+470
+pub fn ensure_dirs(data_dir: &Path) {471
+ for dir in [472
+ data_dir.to_path_buf(),473
+ data_dir.join("sessions"),474
+ data_dir.join("stores"),475
+ ] {476
+ let _ = create_private_dir(&dir);477
+ restrict_dir(&dir);478
+ }479
+}480
+481
+#[cfg(unix)]482
+fn restrict_dir(path: &Path) {483
+ use std::os::unix::fs::PermissionsExt;484
+ let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));485
+}486
+487
+#[cfg(not(unix))]488
+fn restrict_dir(_path: &Path) {}489
+490
+#[cfg(unix)]491
+fn restrict_file(path: &Path) {492
+ use std::os::unix::fs::PermissionsExt;493
+ let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));494
+}495
+496
+#[cfg(not(unix))]497
+fn restrict_file(_path: &Path) {}@@ -0,0 +1,540 @@
1
+use std::{2
+ net::TcpListener,3
+ path::PathBuf,4
+ process::{Child, Command, Stdio},5
+ sync::atomic::{AtomicBool, Ordering},6
+ time::Duration,7
+};8
+9
+use matrix_sdk::test_utils::mocks::MatrixMockServer;10
+use reqwest::{Client, StatusCode, header};11
+use serde_json::{Value, json};12
+use tempfile::TempDir;13
+use wiremock::{14
+ Mock, ResponseTemplate,15
+ matchers::{method, path_regex},16
+};17
+18
+struct Server(Child);19
+20
+impl Drop for Server {21
+ fn drop(&mut self) {22
+ let _ = self.0.kill();23
+ let _ = self.0.wait();24
+ }25
+}26
+27
+fn server_binary() -> PathBuf {28
+ option_env!("CARGO_BIN_EXE_malice")29
+ .map(PathBuf::from)30
+ .unwrap_or_else(|| {31
+ std::env::current_exe()32
+ .expect("integration test executable")33
+ .parent()34
+ .expect("cargo deps directory")35
+ .parent()36
+ .expect("cargo target directory")37
+ .join("malice")38
+ })39
+}40
+41
+async fn start(port: u16, data_dir: &TempDir, client: &Client) -> Server {42
+ let child = Command::new(server_binary())43
+ .env("MALICE_BIND", format!("127.0.0.1:{port}"))44
+ .env("MALICE_DATA_DIR", data_dir.path())45
+ .env("MALICE_SESSION_COOKIE_SECURE", "false")46
+ .env("MALICE_ALLOW_HTTP_HOMESERVER", "true")47
+ .stdout(Stdio::null())48
+ .stderr(Stdio::inherit())49
+ .spawn()50
+ .expect("start Malice backend");51
+ let mut server = Server(child);52
+ tokio::time::timeout(Duration::from_secs(30), async {53
+ loop {54
+ if let Some(status) = server.0.try_wait().expect("poll backend") {55
+ panic!("Malice exited before health check: {status}");56
+ }57
+ if client58
+ .get(format!("http://127.0.0.1:{port}/api/health"))59
+ .send()60
+ .await61
+ .is_ok_and(|response| response.status().is_success())62
+ {63
+ break;64
+ }65
+ tokio::time::sleep(Duration::from_millis(100)).await;66
+ }67
+ })68
+ .await69
+ .expect("Malice did not start");70
+ server71
+}72
+73
+fn unused_port() -> u16 {74
+ TcpListener::bind("127.0.0.1:0")75
+ .expect("bind ephemeral port")76
+ .local_addr()77
+ .expect("read ephemeral port")78
+ .port()79
+}80
+81
+#[tokio::test]82
+async fn login_isolated_cookie_survives_restart_and_logout_revokes_it() {83
+ let matrix = MatrixMockServer::new().await;84
+ matrix.mock_versions().ok().mount().await;85
+ matrix.mock_login().ok().expect(1).mount().await;86
+ let first_room = AtomicBool::new(false);87
+ Mock::given(method("POST"))88
+ .and(path_regex("/sync$"))89
+ .respond_with(move |request: &wiremock::Request| {90
+ let body: Value = serde_json::from_slice(&request.body).unwrap();91
+ let rooms =92
+ if body["conn_id"] == "room-list" && !first_room.swap(true, Ordering::Relaxed) {93
+ json!({"!malice:localhost": {94
+ "initial": true,95
+ "name": "Malice test room",96
+ "joined_count": 1,97
+ "required_state": [{98
+ "type": "m.room.member",99
+ "state_key": "@cheeky_monkey:matrix.org",100
+ "sender": "@cheeky_monkey:matrix.org",101
+ "event_id": "$join:localhost",102
+ "origin_server_ts": 1,103
+ "content": {"membership": "join"}104
+ }],105
+ "timeline": [{106
+ "type": "m.room.message",107
+ "sender": "@cheeky_monkey:matrix.org",108
+ "event_id": "$recent:localhost",109
+ "origin_server_ts": 2,110
+ "content": {"msgtype":"m.text", "body":"recent channel message"}111
+ },{112
+ "type": "m.room.message",113
+ "sender": "@cheeky_monkey:matrix.org",114
+ "event_id": "$image:localhost",115
+ "origin_server_ts": 3,116
+ "content": {117
+ "msgtype": "m.image",118
+ "body": "image.png",119
+ "url": "mxc://localhost/avatar",120
+ "info": {"mimetype": "image/png", "size": 13}121
+ }122
+ }],123
+ "prev_batch": "older-page"124
+ }})125
+ } else {126
+ json!({})127
+ };128
+ ResponseTemplate::new(200)129
+ .set_body_json(json!({"pos":"1","lists":{},"rooms":rooms,"extensions":{}}))130
+ .set_delay(Duration::from_millis(100))131
+ })132
+ .mount(matrix.server())133
+ .await;134
+ Mock::given(method("GET"))135
+ .and(path_regex("/rooms/.*/messages$"))136
+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({137
+ "start": "older-page",138
+ "chunk": [{139
+ "type": "m.room.message",140
+ "room_id": "!malice:localhost",141
+ "sender": "@cheeky_monkey:matrix.org",142
+ "event_id": "$older:localhost",143
+ "origin_server_ts": 1,144
+ "content": {"msgtype":"m.text", "body":"older channel message"}145
+ }]146
+ })))147
+ .mount(matrix.server())148
+ .await;149
+ matrix150
+ .mock_room_state_encryption()151
+ .expect_access_token("abc123")152
+ .plain()153
+ .mount()154
+ .await;155
+ matrix156
+ .mock_room_send()157
+ .expect_access_token("abc123")158
+ .body_matches_partial_json(json!({"body":"backend room smoke"}))159
+ .ok(matrix_sdk::ruma::event_id!("$sent:localhost"))160
+ .expect(1)161
+ .mount()162
+ .await;163
+164
+ Mock::given(method("GET"))165
+ .and(path_regex("/download/[^/]+/avatar$"))166
+ .respond_with(ResponseTemplate::new(200).set_body_bytes(b"\x89PNG\r\n\x1a\nimage".to_vec()))167
+ .mount(matrix.server())168
+ .await;169
+ Mock::given(method("GET"))170
+ .and(path_regex("/download/[^/]+/svg$"))171
+ .respond_with(172
+ ResponseTemplate::new(200).set_body_bytes(b"<svg onload='alert(1)'/>".to_vec()),173
+ )174
+ .mount(matrix.server())175
+ .await;176
+177
+ let data_dir = tempfile::tempdir().expect("temporary user stores");178
+ let client = Client::new();179
+ let port = unused_port();180
+ let base = format!("http://127.0.0.1:{port}");181
+ let server = start(port, &data_dir, &client).await;182
+183
+ let guest: Value = client184
+ .get(format!("{base}/api/session"))185
+ .send()186
+ .await187
+ .unwrap()188
+ .json()189
+ .await190
+ .unwrap();191
+ assert_eq!(guest["user_id"], Value::Null);192
+ assert_eq!(193
+ client194
+ .get(format!("{base}/api/rooms"))195
+ .send()196
+ .await197
+ .unwrap()198
+ .status(),199
+ StatusCode::UNAUTHORIZED200
+ );201
+202
+ let cross_origin = client203
+ .post(format!("{base}/api/login"))204
+ .header(header::ORIGIN, "https://attacker.example")205
+ .json(&json!({206
+ "homeserver": matrix.uri(),207
+ "username": "alice",208
+ "password": "test-password"209
+ }))210
+ .send()211
+ .await212
+ .unwrap();213
+ assert_eq!(cross_origin.status(), StatusCode::FORBIDDEN);214
+215
+ let cross_port = client216
+ .post(format!("{base}/api/login"))217
+ .header(header::ORIGIN, format!("http://127.0.0.1:{}", port + 1))218
+ .json(&json!({219
+ "homeserver": matrix.uri(),220
+ "username": "alice",221
+ "password": "test-password"222
+ }))223
+ .send()224
+ .await225
+ .unwrap();226
+ assert_eq!(cross_port.status(), StatusCode::FORBIDDEN);227
+228
+ let login = client229
+ .post(format!("{base}/api/login"))230
+ .header(header::ORIGIN, &base)231
+ .json(&json!({232
+ "homeserver": matrix.uri(),233
+ "username": "alice",234
+ "password": "test-password"235
+ }))236
+ .send()237
+ .await238
+ .unwrap();239
+ assert!(240
+ login.status().is_success(),241
+ "login returned {}",242
+ login.status()243
+ );244
+ let cookie = login245
+ .headers()246
+ .get(header::SET_COOKIE)247
+ .expect("opaque session cookie")248
+ .to_str()249
+ .unwrap()250
+ .split(';')251
+ .next()252
+ .unwrap()253
+ .to_owned();254
+ let logged_in: Value = login.json().await.unwrap();255
+ assert!(logged_in["user_id"].as_str().is_some());256
+ #[cfg(unix)]257
+ {258
+ use std::os::unix::fs::PermissionsExt;259
+ let sid = cookie.split_once('=').unwrap().1;260
+ let mode = |path: std::path::PathBuf| {261
+ std::fs::metadata(path).unwrap().permissions().mode() & 0o777262
+ };263
+ assert_eq!(mode(data_dir.path().join("master.key")), 0o600);264
+ assert_eq!(mode(data_dir.path().join("sessions")), 0o700);265
+ assert_eq!(266
+ mode(data_dir.path().join("sessions").join(format!("{sid}.bin"))),267
+ 0o600268
+ );269
+ }270
+ assert_eq!(271
+ client272
+ .get(format!("{base}/api/rooms"))273
+ .send()274
+ .await275
+ .unwrap()276
+ .status(),277
+ StatusCode::UNAUTHORIZED,278
+ "one browser must not inherit another browser's Matrix session"279
+ );280
+ assert_eq!(281
+ client282
+ .get(format!("{base}/api/media"))283
+ .query(&[("mxc", "mxc://localhost/avatar")])284
+ .send()285
+ .await286
+ .unwrap()287
+ .status(),288
+ StatusCode::UNAUTHORIZED,289
+ );290
+ assert_eq!(291
+ client292
+ .get(format!("{base}/api/media"))293
+ .header(header::COOKIE, &cookie)294
+ .query(&[("mxc", "not-a-mxc")])295
+ .send()296
+ .await297
+ .unwrap()298
+ .status(),299
+ StatusCode::BAD_REQUEST,300
+ );301
+ let avatar = client302
+ .get(format!("{base}/api/media"))303
+ .header(header::COOKIE, &cookie)304
+ .query(&[("mxc", "mxc://localhost/avatar")])305
+ .send()306
+ .await307
+ .unwrap();308
+ assert_eq!(avatar.status(), StatusCode::OK);309
+ assert_eq!(310
+ avatar.headers().get(header::CONTENT_TYPE).unwrap(),311
+ "image/png"312
+ );313
+ assert_eq!(314
+ avatar.bytes().await.unwrap().as_ref(),315
+ b"\x89PNG\r\n\x1a\nimage"316
+ );317
+ assert_eq!(318
+ client319
+ .get(format!("{base}/api/media"))320
+ .header(header::COOKIE, &cookie)321
+ .query(&[("mxc", "mxc://localhost/svg")])322
+ .send()323
+ .await324
+ .unwrap()325
+ .status(),326
+ StatusCode::UNSUPPORTED_MEDIA_TYPE,327
+ );328
+ let rooms = client329
+ .get(format!("{base}/api/rooms"))330
+ .header(header::COOKIE, &cookie)331
+ .send()332
+ .await333
+ .unwrap();334
+ assert!(rooms.status().is_success());335
+ let rooms: Value = rooms.json().await.unwrap();336
+ assert!(rooms["rooms"].is_array());337
+ tokio::time::timeout(Duration::from_secs(5), async {338
+ loop {339
+ let response: Value = client340
+ .get(format!("{base}/api/rooms"))341
+ .header(header::COOKIE, &cookie)342
+ .send()343
+ .await344
+ .unwrap()345
+ .json()346
+ .await347
+ .unwrap();348
+ if response["rooms"]349
+ .as_array()350
+ .unwrap()351
+ .iter()352
+ .any(|room| room["room_id"] == "!malice:localhost")353
+ {354
+ break;355
+ }356
+ tokio::time::sleep(Duration::from_millis(50)).await;357
+ }358
+ })359
+ .await360
+ .expect("joined room appeared in live room list");361
+ let open = client362
+ .post(format!("{base}/api/rooms/open"))363
+ .header(header::ORIGIN, &base)364
+ .header(header::COOKIE, &cookie)365
+ .json(&json!({"room_id":"!malice:localhost"}))366
+ .send()367
+ .await368
+ .unwrap();369
+ assert_eq!(open.status(), StatusCode::NO_CONTENT);370
+ let media_id = tokio::time::timeout(Duration::from_secs(10), async {371
+ loop {372
+ let mut stream = client373
+ .get(format!("{base}/api/events"))374
+ .header(header::COOKIE, &cookie)375
+ .send()376
+ .await377
+ .unwrap();378
+ let frame = stream.chunk().await.unwrap().expect("SSE snapshot");379
+ let text = std::str::from_utf8(&frame).unwrap();380
+ let payload = text381
+ .lines()382
+ .find_map(|line| line.strip_prefix("data: "))383
+ .unwrap();384
+ let snapshot: Value = serde_json::from_str(payload).unwrap();385
+ if let Some(id) = snapshot["timelines"]["!malice:localhost"]386
+ .as_array()387
+ .and_then(|items| {388
+ items.iter().find_map(|item| {389
+ item["Event"]["content"]["Message"]["media"]["id"].as_str()390
+ })391
+ })392
+ {393
+ break id.to_owned();394
+ }395
+ tokio::time::sleep(Duration::from_millis(50)).await;396
+ }397
+ })398
+ .await399
+ .expect("opened room registered image attachment");400
+ assert_eq!(401
+ client402
+ .get(format!("{base}/api/message-media"))403
+ .query(&[("media_id", media_id.as_str()), ("size", "Full")])404
+ .send()405
+ .await406
+ .unwrap()407
+ .status(),408
+ StatusCode::UNAUTHORIZED,409
+ );410
+ let attachment = client411
+ .get(format!("{base}/api/message-media"))412
+ .header(header::COOKIE, &cookie)413
+ .query(&[("media_id", media_id.as_str()), ("size", "Full")])414
+ .send()415
+ .await416
+ .unwrap();417
+ assert_eq!(attachment.status(), StatusCode::OK);418
+ assert_eq!(419
+ attachment.headers().get(header::CONTENT_TYPE).unwrap(),420
+ "image/png"421
+ );422
+ assert_eq!(423
+ attachment.bytes().await.unwrap().as_ref(),424
+ b"\x89PNG\r\n\x1a\nimage"425
+ );426
+ let sent = client427
+ .post(format!("{base}/api/rooms/send"))428
+ .header(header::ORIGIN, &base)429
+ .header(header::COOKIE, &cookie)430
+ .json(&json!({"room_id":"!malice:localhost", "body":"backend room smoke"}))431
+ .send()432
+ .await433
+ .unwrap();434
+ assert_eq!(sent.status(), StatusCode::NO_CONTENT);435
+ tokio::time::timeout(Duration::from_secs(5), async {436
+ loop {437
+ if matrix438
+ .received_requests()439
+ .await440
+ .unwrap()441
+ .iter()442
+ .any(|request| request.url.path().contains("/send/m.room.message/"))443
+ {444
+ break;445
+ }446
+ tokio::time::sleep(Duration::from_millis(50)).await;447
+ }448
+ })449
+ .await450
+ .expect("message reached Matrix send endpoint");451
+ tokio::time::timeout(Duration::from_secs(15), async {452
+ loop {453
+ let mut stream = client454
+ .get(format!("{base}/api/events"))455
+ .header(header::COOKIE, &cookie)456
+ .send()457
+ .await458
+ .unwrap();459
+ let frame = stream.chunk().await.unwrap().expect("SSE snapshot");460
+ let text = std::str::from_utf8(&frame).unwrap();461
+ let payload = text462
+ .lines()463
+ .find_map(|line| line.strip_prefix("data: "))464
+ .unwrap();465
+ let snapshot: Value = serde_json::from_str(payload).unwrap();466
+ if snapshot["connection"] == "online"467
+ && snapshot["timelines"]["!malice:localhost"]468
+ .as_array()469
+ .is_some_and(|items| {470
+ items.iter().any(|item| {471
+ item["Event"]["content"]["Message"]["body"] == "older channel message"472
+ && item["Event"]["event_id"] == "$older:localhost"473
+ }) && items.iter().any(|item| {474
+ item["Event"]["content"]["Message"]["body"] == "backend room smoke"475
+ && item["Event"]["event_id"] == "$sent:localhost"476
+ && item["Event"]["send_state"] == "Sent"477
+ })478
+ })479
+ {480
+ break;481
+ }482
+ tokio::time::sleep(Duration::from_millis(50)).await;483
+ }484
+ })485
+ .await486
+ .expect("server-confirmed message appeared in online timeline SSE snapshot");487
+488
+ drop(server);489
+ let _restored = start(port, &data_dir, &client).await;490
+ let session: Value = client491
+ .get(format!("{base}/api/session"))492
+ .header(header::COOKIE, &cookie)493
+ .send()494
+ .await495
+ .unwrap()496
+ .json()497
+ .await498
+ .unwrap();499
+ assert_eq!(session["user_id"], logged_in["user_id"]);500
+ let mut events = client501
+ .get(format!("{base}/api/events"))502
+ .header(header::COOKIE, &cookie)503
+ .send()504
+ .await505
+ .unwrap();506
+ assert_eq!(events.status(), StatusCode::OK);507
+ let initial = events.chunk().await.unwrap().expect("initial SSE frame");508
+ assert!(String::from_utf8_lossy(&initial).contains("\"type\":\"snapshot\""));509
+510
+ matrix511
+ .mock_logout()512
+ .expect_access_token("abc123")513
+ .ok()514
+ .expect(1)515
+ .mount()516
+ .await;517
+ let logout = client518
+ .post(format!("{base}/api/logout"))519
+ .header(header::ORIGIN, &base)520
+ .header(header::COOKIE, &cookie)521
+ .send()522
+ .await523
+ .unwrap();524
+ assert_eq!(logout.status(), StatusCode::NO_CONTENT);525
+ tokio::time::timeout(Duration::from_secs(2), async {526
+ while events.chunk().await.unwrap().is_some() {}527
+ })528
+ .await529
+ .expect("logout must close the authenticated event stream");530
+ let after_logout: Value = client531
+ .get(format!("{base}/api/session"))532
+ .header(header::COOKIE, &cookie)533
+ .send()534
+ .await535
+ .unwrap()536
+ .json()537
+ .await538
+ .unwrap();539
+ assert_eq!(after_logout["user_id"], Value::Null);540
+}@@ -0,0 +1,139 @@
1
+use std::{2
+ net::TcpListener,3
+ path::PathBuf,4
+ process::{Child, Command, Stdio},5
+ time::Duration,6
+};7
+8
+use reqwest::{Client, StatusCode, header};9
+use tempfile::TempDir;10
+11
+struct Server(Child);12
+13
+impl Drop for Server {14
+ fn drop(&mut self) {15
+ let _ = self.0.kill();16
+ let _ = self.0.wait();17
+ }18
+}19
+20
+fn server_binary() -> PathBuf {21
+ option_env!("CARGO_BIN_EXE_malice")22
+ .map(PathBuf::from)23
+ .unwrap_or_else(|| {24
+ std::env::current_exe()25
+ .expect("integration test executable")26
+ .parent()27
+ .expect("cargo deps directory")28
+ .parent()29
+ .expect("cargo target directory")30
+ .join("malice")31
+ })32
+}33
+34
+fn unused_port() -> u16 {35
+ TcpListener::bind("127.0.0.1:0")36
+ .expect("bind ephemeral port")37
+ .local_addr()38
+ .expect("read ephemeral port")39
+ .port()40
+}41
+42
+async fn start(port: u16, web_dir: &TempDir, idle_secs: u64, client: &Client) -> Server {43
+ let child = Command::new(server_binary())44
+ .env("MALICE_BIND", format!("127.0.0.1:{port}"))45
+ .env("MALICE_STATIC_ONLY", "1")46
+ .env("MALICE_WEB_DIR", web_dir.path())47
+ .env("MALICE_IDLE_EXIT_SECS", idle_secs.to_string())48
+ .stdout(Stdio::null())49
+ .stderr(Stdio::inherit())50
+ .spawn()51
+ .expect("start Malice backend");52
+ let mut server = Server(child);53
+ tokio::time::timeout(Duration::from_secs(30), async {54
+ loop {55
+ if let Some(status) = server.0.try_wait().expect("poll backend") {56
+ panic!("Malice exited before it started serving: {status}");57
+ }58
+ if client59
+ .get(format!("http://127.0.0.1:{port}/"))60
+ .send()61
+ .await62
+ .is_ok_and(|response| response.status().is_success())63
+ {64
+ break;65
+ }66
+ tokio::time::sleep(Duration::from_millis(100)).await;67
+ }68
+ })69
+ .await70
+ .expect("Malice did not start");71
+ server72
+}73
+74
+#[tokio::test]75
+async fn static_only_serves_the_bundle_and_exits_when_idle() {76
+ let web_dir = tempfile::tempdir().expect("temporary web dir");77
+ std::fs::write(web_dir.path().join("index.html"), "<html>malice</html>")78
+ .expect("write index.html");79
+ std::fs::write(web_dir.path().join("sw.js"), "// service worker").expect("write sw.js");80
+ std::fs::write(web_dir.path().join("style.css"), "body {}").expect("write style.css");81
+82
+ let client = Client::new();83
+ let port = unused_port();84
+ let base = format!("http://127.0.0.1:{port}");85
+ let mut server = start(port, &web_dir, 1, &client).await;86
+87
+ let index = client.get(&base).send().await.unwrap();88
+ assert_eq!(index.status(), StatusCode::OK);89
+ assert_eq!(90
+ index.headers().get(header::CACHE_CONTROL).unwrap(),91
+ "no-cache"92
+ );93
+ assert_eq!(index.headers().get("service-worker-allowed").unwrap(), "/");94
+ assert_eq!(index.text().await.unwrap(), "<html>malice</html>");95
+96
+ let sw = client.get(format!("{base}/sw.js")).send().await.unwrap();97
+ assert_eq!(sw.status(), StatusCode::OK);98
+ assert_eq!(sw.headers().get(header::CACHE_CONTROL).unwrap(), "no-cache");99
+ assert_eq!(sw.headers().get("service-worker-allowed").unwrap(), "/");100
+101
+ let asset = client102
+ .get(format!("{base}/style.css"))103
+ .send()104
+ .await105
+ .unwrap();106
+ assert_eq!(asset.status(), StatusCode::OK);107
+ assert!(asset.headers().get(header::CACHE_CONTROL).is_none());108
+109
+ assert_eq!(110
+ client111
+ .get(format!("{base}/api/rooms"))112
+ .send()113
+ .await114
+ .unwrap()115
+ .status(),116
+ StatusCode::NOT_FOUND,117
+ );118
+ assert_eq!(119
+ client120
+ .get(format!("{base}/api/"))121
+ .send()122
+ .await123
+ .unwrap()124
+ .status(),125
+ StatusCode::NOT_FOUND,126
+ );127
+128
+ let status = tokio::time::timeout(Duration::from_secs(10), async {129
+ loop {130
+ if let Some(status) = server.0.try_wait().expect("poll backend") {131
+ return status;132
+ }133
+ tokio::time::sleep(Duration::from_millis(50)).await;134
+ }135
+ })136
+ .await137
+ .expect("Malice did not exit once idle");138
+ assert!(status.success(), "idle exit should be clean: {status}");139
+}@@ -0,0 +1,3 @@
1
+[target.wasm32-unknown-unknown]2
+runner = "wasm-bindgen-test-runner"3
+rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]@@ -0,0 +1,4532 @@
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.1.0",12
+ "proc-macro2",13
+ "quote",14
+ "syn 3.0.6",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 = "assign"111
+version = "1.1.1"112
+source = "registry+https://github.com/rust-lang/crates.io-index"113
+checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002"114
+115
+[[package]]116
+name = "async-channel"117
+version = "2.5.0"118
+source = "registry+https://github.com/rust-lang/crates.io-index"119
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"120
+dependencies = [121
+ "concurrent-queue",122
+ "event-listener-strategy",123
+ "futures-core",124
+ "pin-project-lite",125
+]126
+127
+[[package]]128
+name = "async-compression"129
+version = "0.4.48"130
+source = "registry+https://github.com/rust-lang/crates.io-index"131
+checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd"132
+dependencies = [133
+ "compression-codecs",134
+ "compression-core",135
+ "pin-project-lite",136
+ "tokio",137
+]138
+139
+[[package]]140
+name = "async-once-cell"141
+version = "0.5.4"142
+source = "registry+https://github.com/rust-lang/crates.io-index"143
+checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"144
+145
+[[package]]146
+name = "async-rx"147
+version = "0.2.1"148
+source = "registry+https://github.com/rust-lang/crates.io-index"149
+checksum = "a82c1a761cd7b772912fd2500f2c0a850c0c72f5f66543038393ac6cfaa3659a"150
+dependencies = [151
+ "futures-core",152
+ "pin-project-lite",153
+]154
+155
+[[package]]156
+name = "async-stream"157
+version = "0.3.6"158
+source = "registry+https://github.com/rust-lang/crates.io-index"159
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"160
+dependencies = [161
+ "async-stream-impl",162
+ "futures-core",163
+ "pin-project-lite",164
+]165
+166
+[[package]]167
+name = "async-stream-impl"168
+version = "0.3.6"169
+source = "registry+https://github.com/rust-lang/crates.io-index"170
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"171
+dependencies = [172
+ "proc-macro2",173
+ "quote",174
+ "syn 2.0.119",175
+]176
+177
+[[package]]178
+name = "async-trait"179
+version = "0.1.92"180
+source = "registry+https://github.com/rust-lang/crates.io-index"181
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"182
+dependencies = [183
+ "proc-macro2",184
+ "quote",185
+ "syn 3.0.6",186
+]187
+188
+[[package]]189
+name = "async_cell"190
+version = "0.2.3"191
+source = "registry+https://github.com/rust-lang/crates.io-index"192
+checksum = "447ab28afbb345f5408b120702a44e5529ebf90b1796ec76e9528df8e288e6c2"193
+dependencies = [194
+ "loom",195
+]196
+197
+[[package]]198
+name = "atomic-waker"199
+version = "1.1.2"200
+source = "registry+https://github.com/rust-lang/crates.io-index"201
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"202
+203
+[[package]]204
+name = "autocfg"205
+version = "1.5.1"206
+source = "registry+https://github.com/rust-lang/crates.io-index"207
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"208
+209
+[[package]]210
+name = "aws-lc-rs"211
+version = "1.18.1"212
+source = "registry+https://github.com/rust-lang/crates.io-index"213
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"214
+dependencies = [215
+ "aws-lc-sys",216
+ "zeroize",217
+]218
+219
+[[package]]220
+name = "aws-lc-sys"221
+version = "0.45.0"222
+source = "registry+https://github.com/rust-lang/crates.io-index"223
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"224
+dependencies = [225
+ "cc",226
+ "cmake",227
+ "dunce",228
+ "fs_extra",229
+ "pkg-config",230
+]231
+232
+[[package]]233
+name = "backon"234
+version = "1.6.0"235
+source = "registry+https://github.com/rust-lang/crates.io-index"236
+checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"237
+dependencies = [238
+ "fastrand",239
+ "gloo-timers",240
+ "tokio",241
+]242
+243
+[[package]]244
+name = "base64"245
+version = "0.22.1"246
+source = "registry+https://github.com/rust-lang/crates.io-index"247
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"248
+249
+[[package]]250
+name = "base64"251
+version = "0.23.1"252
+source = "registry+https://github.com/rust-lang/crates.io-index"253
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"254
+255
+[[package]]256
+name = "base64ct"257
+version = "1.8.3"258
+source = "registry+https://github.com/rust-lang/crates.io-index"259
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"260
+261
+[[package]]262
+name = "bitflags"263
+version = "2.13.2"264
+source = "registry+https://github.com/rust-lang/crates.io-index"265
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"266
+dependencies = [267
+ "serde_core",268
+]269
+270
+[[package]]271
+name = "bitmaps"272
+version = "3.2.1"273
+source = "registry+https://github.com/rust-lang/crates.io-index"274
+checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"275
+276
+[[package]]277
+name = "blake3"278
+version = "1.8.7"279
+source = "registry+https://github.com/rust-lang/crates.io-index"280
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"281
+dependencies = [282
+ "arrayvec",283
+ "cc",284
+ "cfg-if",285
+ "constant_time_eq",286
+ "cpufeatures 0.3.1",287
+]288
+289
+[[package]]290
+name = "block-buffer"291
+version = "0.10.4"292
+source = "registry+https://github.com/rust-lang/crates.io-index"293
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"294
+dependencies = [295
+ "generic-array",296
+]297
+298
+[[package]]299
+name = "block-padding"300
+version = "0.3.3"301
+source = "registry+https://github.com/rust-lang/crates.io-index"302
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"303
+dependencies = [304
+ "generic-array",305
+]306
+307
+[[package]]308
+name = "bs58"309
+version = "0.5.1"310
+source = "registry+https://github.com/rust-lang/crates.io-index"311
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"312
+dependencies = [313
+ "tinyvec",314
+]315
+316
+[[package]]317
+name = "bumpalo"318
+version = "3.20.3"319
+source = "registry+https://github.com/rust-lang/crates.io-index"320
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"321
+322
+[[package]]323
+name = "byteorder"324
+version = "1.5.0"325
+source = "registry+https://github.com/rust-lang/crates.io-index"326
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"327
+328
+[[package]]329
+name = "bytes"330
+version = "1.12.1"331
+source = "registry+https://github.com/rust-lang/crates.io-index"332
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"333
+334
+[[package]]335
+name = "bytesize"336
+version = "2.7.0"337
+source = "registry+https://github.com/rust-lang/crates.io-index"338
+checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b"339
+340
+[[package]]341
+name = "cast"342
+version = "0.3.0"343
+source = "registry+https://github.com/rust-lang/crates.io-index"344
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"345
+346
+[[package]]347
+name = "cbc"348
+version = "0.1.2"349
+source = "registry+https://github.com/rust-lang/crates.io-index"350
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"351
+dependencies = [352
+ "cipher",353
+]354
+355
+[[package]]356
+name = "cc"357
+version = "1.5.1"358
+source = "registry+https://github.com/rust-lang/crates.io-index"359
+checksum = "f360145194ee8e21db5ee7f3fshow full diff (262144 bytes truncated)