atagen

macltok

public
Star0
Watch0

Commit 96fdf90aacd6

tarball

atagen <boss@atagen.co>2026-09-24 12:21 UTC

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

show full diff (262144 bytes truncated)