atagen

malice

public
Star0
Watch0

Commit 973f8fbdaf80

tarball

atagen <boss@atagen.co>2026-10-22 13:19 UTC

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

show full diff (262144 bytes truncated)