diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..849ac6a93f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,32 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and Test Commands +- Build: `cargo make debug` (debug) or `cargo make release` (optimized) +- Format: `cargo make format` or check with `cargo make format-check` +- Lint: `cargo make clippy` +- Test: `cargo make test` (standard) or `cargo make test-all` (all backends) +- Unit tests only: `cargo make unit` +- Single test: `cargo nextest run -p sos-integration-tests test_name` +- CLI tests: `cargo make test-cli` or `cargo make test-shell` +- Documentation: `cargo make doc` + +## Code Style +- Use the Rust 2018 edition style +- Maximum line width: 78 characters +- Prefer `thiserror` for error handling with clear, specific error types +- Use doc comments (`///`) for public APIs +- Add `#![deny(missing_docs)]` and `#![forbid(unsafe_code)]` to crate roots +- Prefer `Result` with custom error types for error handling +- Implement informative Display/Debug traits for public types +- Use bitflags for flag-based enums +- Ensure proper serialization/deserialization for custom types + +## Naming and Structure +- Use snake_case for variables, functions, and modules +- Use PascalCase for types and traits +- Group related functionality in modules +- Re-export important types in crate root +- Use clear, descriptive names that reflect domain concepts +- Follow vault, folder, secret terminology as defined in documentation \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5c64a42d71..b866762a6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,9 +132,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-build" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a133d38cebf328adaea4bc1891d9568e14a394b50e4f4ba5f63dc14e8beaaee9" +checksum = "29082cbb17a6bd7110eac366e11877ac418a2e47719be68c34050ce03aa63c4c" dependencies = [ "windows-sys 0.52.0", ] @@ -236,22 +236,23 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arboard" -version = "3.4.1" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" dependencies = [ "clipboard-win", "log", - "objc2", + "objc2 0.6.1", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.1", "parking_lot", + "percent-encoding", "x11rb", ] @@ -297,7 +298,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "synstructure", ] @@ -309,14 +310,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "async-compression" -version = "0.4.20" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "flate2", "futures-core", @@ -348,7 +349,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -365,13 +366,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -403,9 +404,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ "axum-core", "base64 0.22.1", @@ -432,7 +433,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-tungstenite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -440,12 +441,12 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", @@ -460,9 +461,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" dependencies = [ "axum", "axum-core", @@ -474,8 +475,9 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", + "rustversion", "serde", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] @@ -496,22 +498,21 @@ dependencies = [ "metrics-exporter-prometheus", "pin-project", "tokio", - "tower 0.5.2", + "tower", "tower-http", ] [[package]] name = "axum-server" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8" +checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" dependencies = [ "arc-swap", "bytes", - "futures-util", + "fs-err", "http", "http-body", - "http-body-util", "hyper", "hyper-util", "pin-project-lite", @@ -520,7 +521,6 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tower 0.4.13", "tower-service", ] @@ -558,9 +558,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base32" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -576,9 +576,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "basic-toml" @@ -667,7 +667,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", ] [[package]] @@ -708,9 +708,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -763,7 +763,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -797,7 +797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45a7298287f1443f422d3f46e8ce9f855e75f0e43c06605adb4c52a262faeabd" dependencies = [ "derive_builder 0.10.2", - "getrandom 0.2.15", + "getrandom 0.2.16", "rand 0.8.5", "thiserror 1.0.69", ] @@ -830,9 +830,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.31" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -840,9 +840,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -853,14 +853,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -927,9 +927,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" -version = "0.2.6" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cookie-factory" @@ -995,9 +995,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1101,9 +1101,9 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.5" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" dependencies = [ "nix 0.29.0", "windows-sys 0.59.0", @@ -1133,7 +1133,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1148,12 +1148,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -1172,16 +1172,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1197,34 +1197,20 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.10", + "darling_core 0.20.11", "quote", - "syn 2.0.99", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", + "syn 2.0.100", ] [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbus" @@ -1252,9 +1238,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "zeroize", @@ -1276,9 +1262,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -1320,10 +1306,10 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1343,7 +1329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1364,6 +1350,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1372,9 +1368,15 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1462,7 +1464,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1473,9 +1475,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1529,12 +1531,12 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" -version = "4.0.3" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c44818c96aec5cadc9dacfb97bbcbcfc19a0de75b218412d56f57fbaab94e439" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if 1.0.0", - "rustix 0.38.44", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -1549,9 +1551,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle", @@ -1592,9 +1594,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1652,9 +1654,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" @@ -1665,6 +1667,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1743,7 +1755,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1799,9 +1811,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1812,14 +1824,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1943,9 +1957,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" dependencies = [ "atomic-waker", "bytes", @@ -1953,7 +1967,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -2068,9 +2082,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -2089,12 +2103,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -2159,9 +2173,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -2169,6 +2183,7 @@ dependencies = [ "http", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", @@ -2192,9 +2207,9 @@ dependencies = [ [[package]] name = "i18n-embed" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0454970a5853f498e686cbd7bf9391aac2244928194780cb7a0af0f41937db6" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" dependencies = [ "arc-swap", "fluent", @@ -2212,11 +2227,10 @@ dependencies = [ [[package]] name = "i18n-embed-fl" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7578cee2940492a648bd60fb49ca85ee8c821a63790e0ef5b604cfed353b2a" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" dependencies = [ - "dashmap", "find-crate", "fluent", "fluent-syntax", @@ -2226,7 +2240,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.99", + "syn 2.0.100", "unic-langid", ] @@ -2240,21 +2254,22 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.0", ] [[package]] @@ -2307,9 +2322,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -2331,9 +2346,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -2352,9 +2367,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -2381,7 +2396,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2413,9 +2428,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.5" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2436,9 +2451,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2483,6 +2498,21 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "intl-memoizer" version = "0.5.2" @@ -2636,7 +2666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5e25f9b861a88faa9d272ca4376e1a13c9a37d36de623f013c7bbb0ae2baa1" dependencies = [ "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2708,9 +2738,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libdbus-sys" @@ -2723,9 +2753,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" [[package]] name = "libredox" @@ -2757,9 +2787,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -2779,9 +2809,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "logos" @@ -2813,7 +2843,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.8.5", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2829,7 +2859,7 @@ dependencies = [ "quote", "regex-syntax 0.8.5", "rustc_version", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2894,9 +2924,9 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7deb012b3b2767169ff203fadb4c6b0b82b947512e5eb9e0b78c2e186ad9e3" +checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" dependencies = [ "ahash", "portable-atomic", @@ -2909,7 +2939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", + "indexmap 2.9.0", "metrics", "metrics-util", "quanta", @@ -2918,16 +2948,16 @@ dependencies = [ [[package]] name = "metrics-util" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd4884b1dd24f7d6628274a2f5ae22465c337c5ba065ec9b6edccddf8acc673" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.15.2", "metrics", "quanta", - "rand 0.8.5", + "rand 0.9.1", "rand_xoshiro", "sketches-ddsketch", ] @@ -2956,9 +2986,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", "simd-adler32", @@ -3193,44 +3223,49 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.9.0", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", + "objc2 0.6.1", + "objc2-core-graphics", + "objc2-foundation 0.3.1", ] [[package]] -name = "objc2-core-data" -version = "0.2.2" +name = "objc2-core-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.9.0", - "block2", - "objc2", - "objc2-foundation", + "dispatch2", + "objc2 0.6.1", ] [[package]] -name = "objc2-core-image" -version = "0.2.2" +name = "objc2-core-graphics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "bitflags 2.9.0", + "dispatch2", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -3248,43 +3283,40 @@ dependencies = [ "bitflags 2.9.0", "block2", "libc", - "objc2", + "objc2 0.5.2", ] [[package]] -name = "objc2-local-authentication" -version = "0.2.2" +name = "objc2-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430605e43490dc3837b7d50d8daedacb9f7926da3935a8cd09651a6a9d071b71" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "objc2-io-surface" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.9.0", - "block2", - "objc2", - "objc2-foundation", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-quartz-core" +name = "objc2-local-authentication" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +checksum = "430605e43490dc3837b7d50d8daedacb9f7926da3935a8cd09651a6a9d071b71" dependencies = [ - "bitflags 2.9.0", "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3307,9 +3339,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -3349,7 +3381,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3446,7 +3478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.7.1", + "indexmap 2.9.0", ] [[package]] @@ -3504,7 +3536,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3537,12 +3569,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", + "indexmap 2.9.0", "quick-xml", "serde", "time", @@ -3623,11 +3655,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy 0.8.24", ] [[package]] @@ -3642,12 +3674,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.30" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3723,14 +3755,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -3743,7 +3775,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "version_check", "yansi", ] @@ -3774,7 +3806,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.99", + "syn 2.0.100", "tempfile", ] @@ -3788,7 +3820,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3909,11 +3941,12 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ "bytes", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -3923,17 +3956,18 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "bytes", - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom 0.3.2", + "rand 0.9.1", "ring", "rustc-hash 2.1.1", "rustls", @@ -3947,9 +3981,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" dependencies = [ "cfg_aliases 0.2.1", "libc", @@ -3961,13 +3995,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radix_trie" version = "0.2.1" @@ -3991,13 +4031,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.23", ] [[package]] @@ -4026,7 +4065,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4035,16 +4074,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.2", ] [[package]] name = "rand_xoshiro" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.9.3", ] [[package]] @@ -4075,11 +4114,17 @@ dependencies = [ "yasna", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.9.0", ] @@ -4125,7 +4170,7 @@ dependencies = [ "quote", "refinery-core", "regex", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4174,9 +4219,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64 0.22.1", "bytes", @@ -4206,7 +4251,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower 0.5.2", + "tower", "tower-service", "url", "wasm-bindgen", @@ -4219,11 +4264,11 @@ dependencies = [ [[package]] name = "retry" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9166d72162de3575f950507683fac47e30f6f2c3836b71b7fbc61aa517c9c5f4" +checksum = "a1e211f878258887b3e65dd3c8ff9f530fe109f441a117ee0cdc27f341355032" dependencies = [ - "rand 0.8.5", + "rand 0.9.1", ] [[package]] @@ -4247,13 +4292,13 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if 1.0.0", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -4281,8 +4326,8 @@ dependencies = [ "gio", "jni", "log", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-local-authentication", "polkit", "retry", @@ -4316,9 +4361,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4327,22 +4372,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.99", + "syn 2.0.100", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" dependencies = [ "sha2", "walkdir", @@ -4399,22 +4444,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.1" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys 0.9.2", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "once_cell", "ring", @@ -4444,9 +4489,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -4489,7 +4534,7 @@ checksum = "327e9d075f6df7e25fbf594f1be7ef55cf0d567a6cb5112eeccbbd51ceb48e0d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4608,14 +4653,14 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" dependencies = [ - "self_cell 1.1.0", + "self_cell 1.2.0", ] [[package]] name = "self_cell" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" @@ -4628,9 +4673,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -4649,13 +4694,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4717,7 +4762,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4761,7 +4806,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -4775,10 +4820,10 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4857,9 +4902,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -4903,9 +4948,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smawk" @@ -4931,9 +4976,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4973,6 +5018,7 @@ dependencies = [ "sos-core", "sos-database", "sos-database-upgrader", + "sos-debug-snapshot", "sos-external-files", "sos-integrity", "sos-login", @@ -5010,7 +5056,7 @@ dependencies = [ "futures", "futures-util", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5093,7 +5139,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "serde", "sos-archive", @@ -5115,6 +5161,21 @@ dependencies = [ "urn", ] +[[package]] +name = "sos-changes" +version = "0.17.0" +dependencies = [ + "futures", + "interprocess", + "rustc_version", + "serde_json", + "sos-core", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "sos-cli-helpers" version = "0.1.1" @@ -5133,7 +5194,7 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "parking_lot", "rustc_version", "secrecy", @@ -5216,7 +5277,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "refinery", "rustc_version", "secrecy", @@ -5249,7 +5310,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5277,6 +5338,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "sos-debug-snapshot" +version = "0.17.0" +dependencies = [ + "futures", + "serde_json", + "sos-archive", + "sos-backend", + "sos-client-storage", + "sos-logs", + "sos-sync", + "sos-vfs", + "thiserror 2.0.12", +] + [[package]] name = "sos-extension-service" version = "0.17.0" @@ -5284,6 +5360,7 @@ dependencies = [ "anyhow", "sos-account", "sos-backend", + "sos-changes", "sos-core", "sos-ipc", "sos-net", @@ -5295,7 +5372,7 @@ dependencies = [ name = "sos-external-files" version = "0.17.0" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-core", "sos-vault", @@ -5313,7 +5390,7 @@ dependencies = [ "futures", "futures-util", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "parking_lot", "rustc_version", "serde", @@ -5350,7 +5427,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "maplit2", "parking_lot", "pretty_assertions", @@ -5360,6 +5437,7 @@ dependencies = [ "sos-account", "sos-audit", "sos-backend", + "sos-changes", "sos-client-storage", "sos-core", "sos-database", @@ -5402,7 +5480,7 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sha2", "sos-backend", @@ -5443,6 +5521,7 @@ dependencies = [ "serde_with", "sos-account", "sos-backend", + "sos-changes", "sos-client-storage", "sos-core", "sos-database", @@ -5457,7 +5536,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-util", - "tower 0.5.2", + "tower", "tracing", "typeshare", "url", @@ -5543,7 +5622,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "prost", "rand 0.8.5", "rs_merkle", @@ -5633,7 +5712,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "prost", "prost-build", "protoc-bin-vendored", @@ -5666,7 +5745,7 @@ name = "sos-reducers" version = "0.17.0" dependencies = [ "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-core", "sos-vault", @@ -5678,7 +5757,7 @@ name = "sos-remote-sync" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-account", "sos-backend", @@ -5756,7 +5835,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "k256", "rustc_version", "rustls", @@ -5799,7 +5878,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5847,7 +5926,7 @@ name = "sos-sync" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "serde", "sos-backend", @@ -5933,7 +6012,7 @@ dependencies = [ "bytes", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "k256", "keychain_parser", "pem", @@ -5986,7 +6065,7 @@ dependencies = [ "ed25519-dalek", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "pem", "rustc_version", "secrecy", @@ -6024,7 +6103,7 @@ name = "sos-web" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "sos-account", @@ -6116,9 +6195,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.99" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -6142,7 +6221,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6166,15 +6245,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.18.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if 1.0.0", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", - "rustix 1.0.1", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -6191,11 +6269,11 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 0.38.44", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -6237,7 +6315,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6248,7 +6326,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6263,9 +6341,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -6281,15 +6359,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -6341,9 +6419,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -6364,7 +6442,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6435,9 +6513,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", @@ -6483,7 +6561,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "toml_datetime", "winnow 0.5.40", ] @@ -6494,18 +6572,18 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.3", + "winnow 0.7.6", ] [[package]] name = "totp-rs" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" +checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72" dependencies = [ "base32", "constant_time_eq", @@ -6519,21 +6597,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -6610,7 +6673,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6682,7 +6745,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.0", + "rand 0.9.1", "rustls", "rustls-pki-types", "sha1", @@ -6701,9 +6764,9 @@ dependencies = [ [[package]] name = "typed-generational-arena" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3478ec5cc6caaa9ed86791e8970e320841c3362a7a14b81a5c5c3f9e254b8a44" +checksum = "8c27e0b89f359e864283feca64a75f1cb249370ea97bf42a451521b696ca17cc" dependencies = [ "cfg-if 0.1.10", "nonzero_ext", @@ -6735,7 +6798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" dependencies = [ "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6876,7 +6939,7 @@ version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_json", "utoipa-gen", @@ -6890,7 +6953,7 @@ checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "uuid", ] @@ -6908,11 +6971,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.15.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.2", "serde", ] @@ -6924,9 +6987,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcard4" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f37cbf15f76a5cb6bf6a4d7d6004471cfe0974eac80605fee1cb8c837c9df5d" +checksum = "a7529ce5655c3d5da5e738bb887ae158ed70009baa3591fc58b12358d0adb0bd" dependencies = [ "aho-corasick", "base64 0.22.1", @@ -6987,9 +7050,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -7016,7 +7079,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -7051,7 +7114,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7107,6 +7170,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -7159,23 +7228,27 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.56.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.4.0", ] [[package]] @@ -7186,7 +7259,18 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -7197,24 +7281,35 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.2.0", - "windows-strings", - "windows-targets 0.52.6", + "windows-result 0.3.2", + "windows-strings 0.3.1", + "windows-targets 0.53.0", ] [[package]] @@ -7228,21 +7323,29 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", ] [[package]] @@ -7333,13 +7436,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -7358,6 +7477,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -7382,6 +7507,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -7406,12 +7537,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -7436,6 +7579,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -7460,6 +7609,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -7478,6 +7633,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -7502,6 +7663,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.5.40" @@ -7513,18 +7680,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.0", ] @@ -7601,9 +7768,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "yansi" @@ -7640,7 +7807,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "synstructure", ] @@ -7650,17 +7817,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive 0.7.35", ] [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "zerocopy-derive 0.8.23", + "zerocopy-derive 0.8.24", ] [[package]] @@ -7671,18 +7837,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -7702,7 +7868,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "synstructure", ] @@ -7723,7 +7889,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -7745,7 +7911,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 07a94b5ce7..1ead650a92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,13 @@ members = [ "crates/artifact", "crates/audit", "crates/backend", + "crates/changes", "crates/cli_helpers", "crates/clipboard", "crates/core", "crates/database", "crates/database_upgrader", + "crates/debug_snapshot", "crates/extension_service", "crates/external_files", "crates/filesystem", @@ -45,7 +47,7 @@ members = [ "tests/command_line", "tests/integration", "tests/unit", - "tests/utils", + "tests/utils", ] [workspace.dependencies] @@ -56,8 +58,10 @@ sos-archive = { version = "0.17", path = "crates/archive" } sos-backend = { version = "0.17", path = "crates/backend" } sos-cli-helpers = { version = "0.1", path = "crates/cli_helpers" } sos-core = { version = "0.17", path = "crates/core" } +sos-changes = { version = "0.17", path = "crates/changes" } sos-database = { version = "0.17", path = "crates/database" } sos-database-upgrader = { version = "0.17", path = "crates/database_upgrader" } +sos-debug-snapshot = { version = "0.17", path = "crates/debug_snapshot" } sos-external-files = { version = "0.17.0", path = "crates/external_files" } sos-filesystem = { version = "0.17", path = "crates/filesystem" } sos-integrity = { version = "0.17", path = "crates/integrity" } @@ -136,6 +140,9 @@ hex = { version = "0.4", features = ["serde"] } k256 = { version = "0.13.1", features = ["ecdsa"] } ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } +# ipc changes +interprocess = "2" + # matches the version in k256 sha1 = "0.10.6" sha2 = "0.10.6" diff --git a/Makefile.toml b/Makefile.toml index 68f3ad5d7b..f97224bfb0 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -110,6 +110,7 @@ mkdir -p target/demo ''' [tasks.test-command-line] +script_runner = "@shell" script = ''' export SOS_TEST=1 export PATH="../../target/debug:$PATH" @@ -136,17 +137,20 @@ command = "cargo" args = ["build", "-p", "sos-integration-tests"] [tasks.clean-tests] +script_runner = "@shell" script = ''' rm -rf target/integration-test ''' [tasks.test] +script_runner = "@shell" script = ''' cargo nextest run -p sos-integration-tests -p sos-unit-tests ''' dependencies = ["clean-tests", "build-test"] [tasks.test-all] +script_runner = "@shell" script = ''' cargo nextest run \ -p sos-integration-tests -p sos-unit-tests @@ -160,6 +164,7 @@ SOS_TEST_CLIENT_DB=1 SOS_TEST_SERVER_DB=1 cargo nextest run \ dependencies = ["clean-tests", "build-test"] [tasks.ci] +script_runner = "@shell" script = ''' cargo nextest run --profile ci \ -p sos-integration-tests -p sos-unit-tests @@ -173,6 +178,7 @@ SOS_TEST_CLIENT_DB=1 SOS_TEST_SERVER_DB=1 cargo nextest run --profile ci \ dependencies = ["clean-tests", "build-test"] [tasks.cover] +script_runner = "@shell" script = ''' cargo llvm-cov clean --workspace cargo llvm-cov nextest \ diff --git a/crates/account/src/account_switcher.rs b/crates/account/src/account_switcher.rs index a54dfead9d..9e754232a8 100644 --- a/crates/account/src/account_switcher.rs +++ b/crates/account/src/account_switcher.rs @@ -1,10 +1,10 @@ use crate::{Account, Error, LocalAccount, Result}; +use sos_backend::BackendTarget; use sos_core::{AccountId, Paths}; use sos_login::PublicIdentity; -use sos_vault::list_accounts; use std::pin::Pin; +use std::sync::Arc; use std::{collections::HashMap, future::Future}; -use std::{path::PathBuf, sync::Arc}; #[cfg(feature = "search")] use sos_search::{ArchiveFilter, Document, DocumentView, QueryFilter}; @@ -101,7 +101,7 @@ where pub async fn load_accounts( &mut self, builder: B, - data_dir: Option<&PathBuf>, + target: BackendTarget, ) -> Result<()> where B: Fn( @@ -109,24 +109,11 @@ where ) -> Pin>>>, { - Paths::scaffold(data_dir).await?; - - let paths = if let Some(data_dir) = data_dir { - Paths::new_client(data_dir) - } else { - Paths::new_client(Paths::data_dir()?) - }; - - let identities = list_accounts(Some(&paths)).await?; - + let identities = target.list_accounts().await?; for identity in identities { tracing::info!( account_id = %identity.account_id(), "add_account"); let account = builder(identity).await.unwrap(); - - let paths = account.paths(); - // tracing::info!(paths = ?paths); - paths.ensure().await?; self.add_account(account); } Ok(()) diff --git a/crates/account/src/builder.rs b/crates/account/src/builder.rs index ded592c1bf..ca8cff4be0 100644 --- a/crates/account/src/builder.rs +++ b/crates/account/src/builder.rs @@ -9,7 +9,7 @@ use sos_core::{ DEFAULT_CONTACTS_VAULT_NAME, }, crypto::AccessKey, - AccountId, Paths, SecretId, VaultFlags, VaultId, + AccountId, SecretId, VaultFlags, VaultId, }; use sos_login::{DelegatedAccess, FolderKeys, Identity, IdentityFolder}; use sos_vault::{ @@ -300,12 +300,6 @@ impl AccountBuilder { /// Create a new account and write the identity /// folder to backend storage. pub async fn finish(mut self) -> Result { - // TODO: remove this and always scaffold in test specs - #[cfg(debug_assertions)] - if let BackendTarget::FileSystem(paths) = &self.target { - Paths::scaffold(paths.documents_dir()).await?; - } - // Prepare the identity folder let identity_folder = IdentityFolder::new( self.target.clone(), diff --git a/crates/account/src/local_account.rs b/crates/account/src/local_account.rs index 5d549c4fac..a0cff9ec38 100644 --- a/crates/account/src/local_account.rs +++ b/crates/account/src/local_account.rs @@ -23,8 +23,8 @@ use sos_core::{ device::{DevicePublicKey, TrustedDevice}, encode, events::{ - AccountEvent, DeviceEvent, Event, EventKind, EventLog, EventRecord, - ReadEvent, WriteEvent, + changes_feed, AccountEvent, DeviceEvent, Event, EventKind, EventLog, + EventRecord, LocalChangeEvent, ReadEvent, WriteEvent, }, AccountId, AccountRef, AuthenticationError, FolderRef, Paths, SecretId, UtcDateTime, VaultCommit, VaultFlags, VaultId, @@ -188,6 +188,9 @@ impl LocalAccount { storage.create_account(&public_account).await?; tracing::debug!("new_account::created"); + changes_feed() + .send_replace(LocalChangeEvent::AccountCreated(account_id)); + Ok(Self { account_id, paths: target.paths(), @@ -941,7 +944,6 @@ impl Account for LocalAccount { // Ensure all paths before sign_in let paths = self.paths().with_account_id(account_id); - paths.ensure().await?; tracing::debug!( account_id = %account_id, @@ -1071,6 +1073,11 @@ impl Account for LocalAccount { } self.sign_out().await?; + + changes_feed().send_replace(LocalChangeEvent::AccountDeleted( + *self.account_id(), + )); + Ok(()) } @@ -1161,7 +1168,8 @@ impl Account for LocalAccount { let vault = { let event_log = self.identity_log().await?; let mut log_file = event_log.write().await; - compact_folder(identity.id(), &mut *log_file).await?; + compact_folder(self.account_id(), identity.id(), &mut *log_file) + .await?; let vault = FolderReducer::new() .reduce(&*log_file) @@ -1172,8 +1180,6 @@ impl Account for LocalAccount { vault }; - // TODO: do we need to re-import the identity vault here? - let event = { let event = AccountEvent::UpdateIdentity(encode(&vault).await?); let log = self.account_log().await?; diff --git a/crates/archive/src/writer.rs b/crates/archive/src/writer.rs index f3f10b8a0a..b75133e4ed 100644 --- a/crates/archive/src/writer.rs +++ b/crates/archive/src/writer.rs @@ -1,33 +1,25 @@ -use crate::{Result, ARCHIVE_MANIFEST}; +use crate::Result; use async_zip::{ tokio::write::ZipFileWriter, Compression, ZipDateTimeBuilder, ZipEntryBuilder, }; -use serde::Serialize; use time::OffsetDateTime; use tokio::io::AsyncWrite; use tokio_util::compat::Compat; /// Write to an archive. -pub struct Writer { +pub struct Writer { writer: ZipFileWriter, - manifest: M, } -impl Writer { - /// Create a new writer. - pub fn new(inner: W, manifest: M) -> Self { +impl Writer { + /// Create a new writer with a manifest. + pub fn new(inner: W) -> Self { Self { writer: ZipFileWriter::with_tokio(inner), - manifest, } } - /// Mutable archive manifest. - pub fn manifest_mut(&mut self) -> &mut M { - &mut self.manifest - } - /// Add a file to the archive. pub async fn add_file( &mut self, @@ -42,14 +34,6 @@ impl Writer { self.append_file_buffer(path, content).await } - /// Add the manifest and finish building the archive. - pub async fn finish(mut self) -> Result> { - let manifest = serde_json::to_vec_pretty(&self.manifest)?; - self.append_file_buffer(ARCHIVE_MANIFEST, manifest.as_slice()) - .await?; - Ok(self.writer.close().await?) - } - async fn append_file_buffer( &mut self, path: &str, @@ -73,4 +57,9 @@ impl Writer { self.writer.write_entry_whole(entry, buffer).await?; Ok(()) } + + /// Finish building the archive. + pub async fn finish(self) -> Result> { + Ok(self.writer.close().await?) + } } diff --git a/crates/audit/src/event.rs b/crates/audit/src/event.rs index 9b4be48bd9..6fb6dfa935 100644 --- a/crates/audit/src/event.rs +++ b/crates/audit/src/event.rs @@ -40,6 +40,7 @@ bitflags! { /// * 20 bytes for the public account_id. /// * 16, 32 or 64 bytes for the context data (one, two or four UUIDs). #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct AuditEvent { /// Time the event was created. pub(crate) time: UtcDateTime, @@ -202,7 +203,7 @@ impl From<(&AccountId, &AccountEvent)> for AuditEvent { /// Associated data for an audit log record. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] pub enum AuditData { /// Data for an associated vault. Vault(VaultId), diff --git a/crates/backend/src/audit.rs b/crates/backend/src/audit.rs index d45e568141..103b1ad514 100644 --- a/crates/backend/src/audit.rs +++ b/crates/backend/src/audit.rs @@ -26,17 +26,17 @@ pub fn providers<'a>() -> Option<&'a AuditProviders> { pub async fn append_audit_events(events: &[AuditEvent]) -> Result<()> { #[cfg(not(debug_assertions))] { - let providers = PROVIDERS - .get() - .ok_or_else(|| Error::AuditProvidersNotConfigured)?; + let providers = + providers().ok_or_else(|| Error::AuditProvidersNotConfigured)?; for provider in providers { provider.append_audit_events(events).await?; } } + + // For test specs we don't require an audit trail #[cfg(debug_assertions)] { - let providers = PROVIDERS.get(); - if let Some(providers) = providers { + if let Some(providers) = providers() { for provider in providers { provider.append_audit_events(events).await?; } diff --git a/crates/backend/src/compact.rs b/crates/backend/src/compact.rs index b3e73cfa58..feb6004737 100644 --- a/crates/backend/src/compact.rs +++ b/crates/backend/src/compact.rs @@ -3,7 +3,7 @@ use crate::{BackendEventLog, Error, FolderEventLog, Result}; use sos_core::{ events::{ patch::{FolderDiff, Patch}, - EventLog, EventRecord, + EventLog, EventLogType, EventRecord, }, AccountId, VaultId, }; @@ -20,6 +20,7 @@ use tempfile::NamedTempFile; /// Compact a folder event log. pub async fn compact_folder( + account_id: &AccountId, folder_id: &VaultId, event_log: &mut FolderEventLog, ) -> Result<()> { @@ -38,12 +39,9 @@ pub async fn compact_folder( // Ensure the foreign key constrains exist // in the temporary database - let temp_account_id = AccountId::random(); let temp_name = "compact_temp"; - let account_row = AccountRow::new_insert( - &temp_account_id, - temp_name.to_owned(), - )?; + let account_row = + AccountRow::new_insert(account_id, temp_name.to_owned())?; let mut vault = Vault::default(); *vault.header_mut().id_mut() = *folder_id; let folder_row = FolderRow::new_insert(&vault).await?; @@ -64,7 +62,7 @@ pub async fn compact_folder( // Copy the event log using the new temporary owner let mut temp_event_log = event_log.with_new_client( client, - Some(EventLogOwner::Folder(folder_record)), + Some(EventLogOwner::Folder(*account_id, folder_record)), ); temp_event_log.apply(events.as_slice()).await?; @@ -92,8 +90,12 @@ pub async fn compact_folder( // Apply them to a temporary event log file let temp = NamedTempFile::new()?; - let mut temp_event_log = - FsFolderEventLog::::new_folder(temp.path()).await?; + let mut temp_event_log = FsFolderEventLog::::new_folder( + temp.path(), + *account_id, + EventLogType::Folder(*folder_id), + ) + .await?; temp_event_log.apply(events.as_slice()).await?; let mut records = Vec::new(); diff --git a/crates/backend/src/event_log.rs b/crates/backend/src/event_log.rs index d89566be8b..c60cf8c93f 100644 --- a/crates/backend/src/event_log.rs +++ b/crates/backend/src/event_log.rs @@ -6,7 +6,8 @@ use sos_core::{ commit::{CommitHash, CommitProof, CommitTree}, events::{ patch::{CheckedPatch, Diff, Patch}, - AccountEvent, DeviceEvent, EventLog, EventRecord, WriteEvent, + AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, + WriteEvent, }, AccountId, VaultId, }; @@ -50,6 +51,7 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_account( paths.with_account_id(account_id).account_events(), + *account_id, ) .await?, ), @@ -77,6 +79,8 @@ impl BackendEventLog { paths .with_account_id(account_id) .event_log_path(folder_id), + *account_id, + EventLogType::Folder(*folder_id), ) .await?, ), @@ -100,6 +104,8 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_folder( paths.with_account_id(account_id).identity_events(), + *account_id, + EventLogType::Identity, ) .await?, ), @@ -139,6 +145,7 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_device( paths.with_account_id(account_id).device_events(), + *account_id, ) .await?, ), @@ -164,6 +171,7 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_file( paths.with_account_id(account_id).file_events(), + *account_id, ) .await?, ), diff --git a/crates/backend/src/folder.rs b/crates/backend/src/folder.rs index 11c3962225..97380480f5 100644 --- a/crates/backend/src/folder.rs +++ b/crates/backend/src/folder.rs @@ -4,7 +4,7 @@ use sos_core::{ commit::{CommitHash, CommitState}, crypto::AccessKey, encode, - events::{EventLog, EventRecord, ReadEvent, WriteEvent}, + events::{EventLog, EventLogType, EventRecord, ReadEvent, WriteEvent}, AccountId, VaultFlags, VaultId, }; use sos_core::{constants::EVENT_LOG_EXT, decode, VaultCommit}; @@ -43,6 +43,8 @@ impl Folder { BackendTarget::FileSystem(paths) => { Self::from_path( paths.with_account_id(account_id).vault_path(folder_id), + account_id, + EventLogType::Folder(*folder_id), ) .await } @@ -132,13 +134,21 @@ impl Folder { /// and if an event log does not exist it is created. /// /// If an event log exists the commit tree is loaded into memory. - pub async fn from_path(path: impl AsRef) -> Result { + pub async fn from_path( + path: impl AsRef, + account_id: &AccountId, + log_type: EventLogType, + ) -> Result { let mut events_path = path.as_ref().to_owned(); events_path.set_extension(EVENT_LOG_EXT); let mut event_log = - sos_filesystem::FolderEventLog::::new_folder(events_path) - .await?; + sos_filesystem::FolderEventLog::::new_folder( + events_path, + *account_id, + log_type, + ) + .await?; event_log.load_tree().await?; let needs_init = event_log.tree().root().is_none(); diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index d653ea8353..1863d2cce7 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -98,6 +98,23 @@ impl fmt::Display for BackendTarget { } } +impl fmt::Debug for BackendTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", { + match self { + Self::FileSystem(paths) => format!( + "file:{}", + paths.documents_dir().to_string_lossy() + ), + Self::Database(paths, _) => format!( + "sqlite:{}", + paths.database_file().to_string_lossy() + ), + } + }) + } +} + impl BackendTarget { /// Infer and initialize a new backend target. /// @@ -168,7 +185,6 @@ impl BackendTarget { pub async fn dump_info(&self) -> Result<()> { tracing::debug!( backend_target = %self, - data_dir = %self.paths().documents_dir().display(), "backend::dump_info", ); diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml new file mode 100644 index 0000000000..fdf7c7e5bb --- /dev/null +++ b/crates/changes/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "sos-changes" +version = "0.17.0" +edition = "2021" +description = "Local socket change event producer and consumer for the Save Our Secrets SDK." +homepage = "https://saveoursecrets.com" +license = "MIT OR Apache-2.0" +repository = "https://github.com/saveoursecrets/sdk" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +changes-consumer = ["dep:interprocess", "tokio-util"] +changes-producer = ["dep:interprocess", "tokio-util"] + +[dependencies] +thiserror.workspace = true +sos-core.workspace = true +tracing.workspace = true +futures.workspace = true +tokio.workspace = true +serde_json.workspace = true +interprocess = { workspace = true, optional = true, features = ["tokio"] } +tokio-util = { workspace = true, optional = true, features = ["codec"] } + +[build-dependencies] +rustc_version.workspace = true diff --git a/crates/changes/build.rs b/crates/changes/build.rs new file mode 100644 index 0000000000..5976a1c6d5 --- /dev/null +++ b/crates/changes/build.rs @@ -0,0 +1,14 @@ +use rustc_version::{version_meta, Channel}; + +fn main() { + println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); + + // Set cfg flags depending on release channel + let channel = match version_meta().unwrap().channel { + Channel::Stable => "CHANNEL_STABLE", + Channel::Beta => "CHANNEL_BETA", + Channel::Nightly => "CHANNEL_NIGHTLY", + Channel::Dev => "CHANNEL_DEV", + }; + println!("cargo:rustc-cfg={}", channel); +} diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs new file mode 100644 index 0000000000..73d6020cbe --- /dev/null +++ b/crates/changes/src/consumer.rs @@ -0,0 +1,128 @@ +//! Consumer for change notifications on a local socket. +use crate::{Error, Result, SocketFile}; +use futures::stream::StreamExt; +use interprocess::local_socket::{ + tokio::prelude::*, GenericNamespaced, ListenerOptions, +}; +use sos_core::{events::LocalChangeEvent, Paths}; +use std::{path::PathBuf, sync::Arc}; +use tokio::{ + select, + sync::{mpsc, watch}, +}; +use tokio_util::codec::LengthDelimitedCodec; + +/// Handle to a consumer. +/// +/// Provides access to a receive channel for +/// incoming change events and can also be +/// used to cancel the listener. +pub struct ConsumerHandle { + receiver: mpsc::Receiver, + cancel_tx: watch::Sender, +} + +impl ConsumerHandle { + /// Channel for change events. + pub fn changes(&mut self) -> &mut mpsc::Receiver { + &mut self.receiver + } + + /// Stop listening for incoming events. + pub fn cancel(&self) { + self.cancel_tx.send_replace(true); + } +} + +/// Consumer socket connection for change events. +pub struct ChangeConsumer; + +impl ChangeConsumer { + /// Listen for incoming change events. + /// + /// Returns a handle that can be used to consume the + /// incoming events and stop listening. + pub fn listen(paths: Arc) -> Result { + let path = socket_file(paths)?; + tracing::trace!( + socket_file = %path.display(), + "changes::consumer::listen", + ); + let ps_name = std::process::id().to_string(); + let file = SocketFile::from(path); + let name = ps_name.to_ns_name::()?; + let opts = ListenerOptions::new().name(name); + let listener = match opts.create_tokio() { + Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => { + tracing::error!( + socket_file = %file.as_ref().display(), + "changes::consumer::listen::addr_in_use", + ); + return Err(e.into()); + } + x => x?, + }; + + let (cancel_tx, mut cancel_rx) = watch::channel(false); + let (tx, rx) = mpsc::channel(32); + + // Create the marker file so producers know + // which processes to send change events to + std::fs::File::create(file.as_ref())?; + + #[allow(unreachable_code)] + tokio::task::spawn(async move { + // Keep the RAII file guard alive + let _guard = file; + loop { + select! { + _ = cancel_rx.changed() => { + if *cancel_rx.borrow_and_update() { + break; + } + } + socket = listener.accept() => { + let socket = socket?; + let tx = tx.clone(); + tokio::task::spawn(async move { + let mut reader = LengthDelimitedCodec::builder() + .native_endian() + .new_read(socket); + while let Some(Ok(buffer)) = reader.next().await { + let event: LocalChangeEvent = + serde_json::from_slice(&buffer)?; + if let Err(e) = tx.send(event).await { + tracing::warn!( + error = %e, + "changes::consumer::send_error"); + } + } + + Ok::<_, Error>(()) + }); + } + } + } + Ok::<_, Error>(()) + }); + Ok(ConsumerHandle { + receiver: rx, + cancel_tx, + }) + } +} + +/// Standard path for a consumer socket file. +/// +/// If the parent directory for socket files does not +/// exist it is created. +fn socket_file(paths: std::sync::Arc) -> Result { + let socks = paths.documents_dir().join(crate::SOCKS); + if !socks.exists() { + std::fs::create_dir(&socks)?; + } + let pid = std::process::id(); + let mut path = socks.join(pid.to_string()); + path.set_extension(crate::SOCK_EXT); + Ok(path) +} diff --git a/crates/changes/src/error.rs b/crates/changes/src/error.rs new file mode 100644 index 0000000000..d77fe62b1b --- /dev/null +++ b/crates/changes/src/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +/// Errors generated by the library. +#[derive(Debug, Error)] +pub enum Error { + /// Errors generated by the IO module. + #[error(transparent)] + Io(#[from] std::io::Error), + + /// Errors generated by the JSON module. + #[error(transparent)] + Json(#[from] serde_json::Error), +} diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs new file mode 100644 index 0000000000..02cdb4d93a --- /dev/null +++ b/crates/changes/src/lib.rs @@ -0,0 +1,44 @@ +//! Local socket change notification producer and consumer. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] + +mod error; +pub use error::Error; + +#[cfg(feature = "changes-consumer")] +pub mod consumer; +#[cfg(feature = "changes-producer")] +pub mod producer; + +pub(crate) type Result = std::result::Result; + +use std::path::PathBuf; + +pub(crate) const SOCKS: &str = "socks"; +pub(crate) const SOCK_EXT: &str = "sock"; + +/// Socket file. +pub(crate) struct SocketFile(PathBuf); + +impl From for SocketFile { + fn from(value: PathBuf) -> Self { + Self(value) + } +} + +impl AsRef for SocketFile { + fn as_ref(&self) -> &PathBuf { + &self.0 + } +} + +impl Drop for SocketFile { + fn drop(&mut self) { + tracing::debug!( + file = %self.0.display(), + "changes::socket_file::drop", + ); + let _ = std::fs::remove_file(&self.0); + } +} diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs new file mode 100644 index 0000000000..516efd4c73 --- /dev/null +++ b/crates/changes/src/producer.rs @@ -0,0 +1,163 @@ +//! Producer for change notifications on a local socket. +use crate::{Error, Result}; +use futures::sink::SinkExt; +use interprocess::local_socket::{tokio::prelude::*, GenericNamespaced}; +use sos_core::{ + events::{changes_feed, LocalChangeEvent}, + Paths, +}; +use std::{path::PathBuf, sync::Arc, time::Duration}; +use tokio::{ + select, + sync::{watch, Mutex}, + time, +}; +use tokio_util::codec::LengthDelimitedCodec; + +/// Handle to a producer. +pub struct ProducerHandle { + cancel_tx: watch::Sender, +} + +impl ProducerHandle { + /// Stop listening for change events. + pub fn cancel(&self) { + self.cancel_tx.send_replace(true); + } +} + +/// Producer socket connection for change events. +pub struct ChangeProducer; + +impl ChangeProducer { + /// Listen to the changes feed and send change events to + /// active sockets. + /// + /// The poll interval determines how frequently the socket + /// directory is inspected. The directory is searched for files + /// ending in .sock and with valid PIDs as the file stem. + /// + /// If `sysinfo` is supported it is checked to see if the process + /// is running before being included in the list of socket file paths + /// to attempt to notify of change events. + /// + /// Returns a handle that can be used to cancel the listener. + #[allow(unreachable_code)] + pub async fn listen( + paths: Arc, + poll_interval: Duration, + ) -> Result { + tracing::debug!( + documents_dir = %paths.documents_dir().display(), + poll_interval = ?poll_interval, + "changes::producer::listen", + ); + let (cancel_tx, mut cancel_rx) = watch::channel(false); + let tx = changes_feed(); + let mut rx = tx.subscribe(); + let sockets = find_active_sockets(paths.clone()).await?; + let sockets = Mutex::new(sockets); + let mut interval = time::interval(poll_interval); + tokio::task::spawn(async move { + loop { + let paths = paths.clone(); + select! { + // Explicit cancel notification + _ = cancel_rx.changed() => { + if *cancel_rx.borrow_and_update() { + break; + } + } + // Periodically refresh the list of consumer sockets + // to dispatch change events to + _ = interval.tick() => { + let active = find_active_sockets(paths).await?; + let mut sockets = sockets.lock().await; + *sockets = active; + } + // Proxy the change events to the consumer sockets + event = rx.changed() => { + match event { + Ok(_) => { + let event = rx.borrow_and_update().clone(); + let sockets = sockets.lock().await; + dispatch_sockets(event, &*sockets).await?; + } + Err(_) => {} + } + } + } + } + Ok::<_, Error>(()) + }); + Ok(ProducerHandle { cancel_tx }) + } +} + +async fn dispatch_sockets( + event: LocalChangeEvent, + sockets: &[(u32, PathBuf)], +) -> Result<()> { + for (pid, file) in sockets { + let ps_name = pid.to_string(); + let name = ps_name.to_ns_name::()?; + match LocalSocketStream::connect(name).await { + Ok(socket) => { + let mut writer = LengthDelimitedCodec::builder() + .native_endian() + .new_write(socket); + let message = serde_json::to_vec(&event)?; + writer.send(message.into()).await?; + } + Err(e) => { + // If we can't connect to the socket + // then treat the file as stale and + // remove from disc. + // + // This could happen if the consumer + // process aborted abnormally and + // wasn't able to cleanly remove the file. + let _ = std::fs::remove_file(file)?; + tracing::warn!( + pid = %pid, + error = %e, + "changes::producer::connect_error"); + } + } + } + Ok(()) +} + +/// Find active socket files for a producer. +async fn find_active_sockets( + paths: Arc, +) -> Result> { + use std::fs::read_dir; + let mut sockets = Vec::new(); + let socks = paths.documents_dir().join(crate::SOCKS); + if socks.exists() { + tracing::debug!( + socks_dir = %socks.display(), + "changes::producer::find_active_sockets", + ); + for entry in read_dir(&socks)? { + let entry = entry?; + if let Some(stem) = entry.path().file_stem() { + if let Ok(pid) = + stem.to_string_lossy().as_ref().parse::() + { + tracing::debug!( + sock_file_pid = %pid, + "changes::producer::find_active_sockets", + ); + sockets.push((pid, entry.path().to_owned())); + } + } + } + } + tracing::debug!( + sockets_len = %sockets.len(), + "changes::producer::find_active_sockets", + ); + Ok(sockets) +} diff --git a/crates/core/src/commit/mod.rs b/crates/core/src/commit/mod.rs index 46b1bfd758..69c6d617a0 100644 --- a/crates/core/src/commit/mod.rs +++ b/crates/core/src/commit/mod.rs @@ -16,5 +16,18 @@ pub use tree::CommitTree; /// Commit state combines the last commit hash with /// a commit proof. -#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] pub struct CommitState(pub CommitHash, pub CommitProof); + +/// Commit span represents a section of an event log. +/// +/// There can be no before commit hash when applying the first +/// collection of events to an event log. If an empty collection +/// was passed the after commit hash will be `None`. +#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] +pub struct CommitSpan { + /// Commit hash before changes were applied. + pub before: Option, + /// Commit hash after changes were applied. + pub after: Option, +} diff --git a/crates/core/src/events/change.rs b/crates/core/src/events/change.rs new file mode 100644 index 0000000000..675c433d1f --- /dev/null +++ b/crates/core/src/events/change.rs @@ -0,0 +1,44 @@ +use crate::{commit::CommitSpan, events::EventLogType, AccountId}; +use serde::{Deserialize, Serialize}; +use std::sync::OnceLock; +use tokio::sync::watch; + +static CHANGES_FEED: OnceLock> = + OnceLock::new(); + +/// Change event. +/// +/// Used for IPC communication when a process needs +/// to know if changes have been made externally, +/// +/// For example, the browser extension helper executable +/// can detect changes made by the app and update it's +/// view. +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub enum LocalChangeEvent { + /// Changes feed was initialized. + #[default] + Init, + /// Account was created. + AccountCreated(AccountId), + /// Account was modified. + AccountModified { + /// Account identifier. + account_id: AccountId, + /// Type of the event log. + log_type: EventLogType, + /// Span of commit hashes. + commit_span: CommitSpan, + }, + /// Account was deleted. + AccountDeleted(AccountId), +} + +/// Feed of change events. +pub fn changes_feed<'a>() -> &'a watch::Sender { + CHANGES_FEED.get_or_init(|| { + let (tx, _) = watch::channel(LocalChangeEvent::default()); + tx + }) +} diff --git a/crates/core/src/events/mod.rs b/crates/core/src/events/mod.rs index b29ddc7536..e3aa51355e 100644 --- a/crates/core/src/events/mod.rs +++ b/crates/core/src/events/mod.rs @@ -9,6 +9,7 @@ //! an audit trail of actions. mod account; +mod change; mod device; mod event; mod event_kind; @@ -21,6 +22,7 @@ mod record; mod write; pub use account::AccountEvent; +pub use change::{changes_feed, LocalChangeEvent}; pub use device::DeviceEvent; pub use event::Event; pub use event_kind::EventKind; @@ -31,8 +33,11 @@ pub use read::ReadEvent; pub use record::EventRecord; pub use write::WriteEvent; +use serde::{Deserialize, Serialize}; + /// Types of event logs. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] pub enum EventLogType { /// Identity folder event log. Identity, diff --git a/crates/core/src/paths.rs b/crates/core/src/paths.rs index 2862ade3c1..a0c722175b 100644 --- a/crates/core/src/paths.rs +++ b/crates/core/src/paths.rs @@ -338,6 +338,13 @@ impl Paths { path } + /// Path to the global preferences file. + pub fn global_preferences_file(&self) -> PathBuf { + let mut path = self.documents_dir().join(PREFERENCES_FILE); + path.set_extension(JSON_EXT); + path + } + /// Path to the file used to store account-level system messages. /// /// # Panics diff --git a/crates/database/sql_migrations/V2__global_config.sql b/crates/database/sql_migrations/V2__global_config.sql new file mode 100644 index 0000000000..bd5880ae42 --- /dev/null +++ b/crates/database/sql_migrations/V2__global_config.sql @@ -0,0 +1,16 @@ +-- Global config. +-- +-- Application wide configuration settings such as the binary encoding +-- version. We also include an encoding version for vaults but we should +-- always be using the same encoding across the app so it's better to +-- store it here. +CREATE TABLE IF NOT EXISTS global_config +( + config_id INTEGER PRIMARY KEY NOT NULL, + created_at DATETIME NOT NULL, + modified_at DATETIME NOT NULL, + -- Binary encoding version + -- Version 1 is the binary stream implementation + -- Version 2 is protobuf encoding + binary_encoding INTEGER NOT NULL DEFAULT 1 +); diff --git a/crates/database/src/archive/error.rs b/crates/database/src/archive/error.rs index ce11cf1a2e..5318b41c12 100644 --- a/crates/database/src/archive/error.rs +++ b/crates/database/src/archive/error.rs @@ -24,13 +24,15 @@ pub enum Error { NoDatabaseFile(PathBuf, String), /// Error generated attempting to import an account that - /// already exists in the target database. - #[error("import failed, account '{0}' does not exist in source db")] + /// does not exist in the source database. + #[error( + "import failed, account '{0}' does not exist in the backup archive" + )] ImportSourceNotExists(AccountId), /// Error generated attempting to import an account that /// already exists in the target database. - #[error("import failed, account '{0}' already exists in target db")] + #[error("import failed, account '{0}' already exists")] ImportTargetExists(AccountId), /// Error generated when the checksum for a database does not diff --git a/crates/database/src/archive/export.rs b/crates/database/src/archive/export.rs index bb591eac72..0ee3417d96 100644 --- a/crates/database/src/archive/export.rs +++ b/crates/database/src/archive/export.rs @@ -2,7 +2,7 @@ use super::{types::ManifestVersion3, Error, Result}; use crate::entity::{AccountEntity, AccountRecord, AccountRow}; use async_sqlite::rusqlite::{backup, Connection}; use sha2::{Digest, Sha256}; -use sos_archive::ZipWriter; +use sos_archive::{ZipWriter, ARCHIVE_MANIFEST}; use sos_core::{ commit::CommitHash, constants::{BLOBS_DIR, DATABASE_FILE}, @@ -34,7 +34,8 @@ pub(crate) async fn create( } let zip_file = vfs::File::create(output.as_ref()).await?; - let mut zip_writer = ZipWriter::new(zip_file, ManifestVersion3::new_v3()); + let mut manifest = ManifestVersion3::new_v3(); + let mut zip_writer = ZipWriter::new(zip_file); // Find blobs that we need to add to the archive let accounts = list_accounts(source_db.as_ref())?; @@ -45,8 +46,7 @@ pub(crate) async fn create( let db_buffer = vfs::read(db_temp.path()).await?; let db_checksum = Sha256::digest(&db_buffer); - zip_writer.manifest_mut().checksum = - CommitHash(db_checksum.as_slice().try_into()?); + manifest.checksum = CommitHash(db_checksum.as_slice().try_into()?); zip_writer.add_file(DATABASE_FILE, &db_buffer).await?; // Add external file blobs to the archive @@ -68,6 +68,11 @@ pub(crate) async fn create( } } + let manifest = serde_json::to_vec_pretty(&manifest)?; + zip_writer + .add_file(ARCHIVE_MANIFEST, manifest.as_slice()) + .await?; + zip_writer.finish().await?; Ok(()) } diff --git a/crates/database/src/audit_provider.rs b/crates/database/src/audit_provider.rs index 9d7ac7573d..2cac910e04 100644 --- a/crates/database/src/audit_provider.rs +++ b/crates/database/src/audit_provider.rs @@ -84,35 +84,43 @@ where std::result::Result, >(16); - self.client - .conn_and_then(move |conn| { - let mut stmt = if reverse { - conn.prepare( - "SELECT * FROM audit_logs ORDER BY log_id DESC", - )? - } else { - conn.prepare( - "SELECT * FROM audit_logs ORDER BY log_id ASC", - )? - }; - let mut rows = stmt.query([])?; + let client = self.client.clone(); + tokio::task::spawn(async move { + client + .conn_and_then(move |conn| { + let mut stmt = if reverse { + conn.prepare( + "SELECT * FROM audit_logs ORDER BY log_id DESC", + )? + } else { + conn.prepare( + "SELECT * FROM audit_logs ORDER BY log_id ASC", + )? + }; + let mut rows = stmt.query([])?; - while let Some(row) = rows.next()? { - let row: AuditRow = row.try_into()?; - let record: AuditRecord = row.try_into()?; - let inner_tx = tx.clone(); - futures::executor::block_on(async move { - if let Err(e) = inner_tx.send(Ok(record.event)).await - { + while let Some(row) = rows.next()? { + if tx.is_closed() { + break; + } + let row: AuditRow = row.try_into()?; + let record: AuditRecord = row.try_into()?; + let inner_tx = tx.clone(); + let res = futures::executor::block_on(async move { + inner_tx.send(Ok(record.event)).await + }); + if let Err(e) = res { tracing::error!(error = %e); + break; } - }); - } + } - Ok::<_, Error>(()) - }) - .await - .map_err(Error::from)?; + Ok::<_, Error>(()) + }) + .await + .map_err(Error::from)?; + Ok::<_, Self::Error>(()) + }); Ok(Box::pin(ReceiverStream::new(rx))) } diff --git a/crates/database/src/event_log.rs b/crates/database/src/event_log.rs index 8917230f2b..c080782135 100644 --- a/crates/database/src/event_log.rs +++ b/crates/database/src/event_log.rs @@ -22,12 +22,13 @@ use futures::{ stream::{BoxStream, StreamExt, TryStreamExt}, }; use sos_core::{ - commit::{CommitHash, CommitProof, CommitTree, Comparison}, + commit::{CommitHash, CommitProof, CommitSpan, CommitTree, Comparison}, encoding::VERSION1, events::{ + changes_feed, patch::{CheckedPatch, Diff, Patch}, AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, - WriteEvent, + LocalChangeEvent, WriteEvent, }, AccountId, VaultId, }; @@ -37,16 +38,26 @@ use sos_core::{ #[doc(hidden)] pub enum EventLogOwner { /// Event log owned by an account. - Account(i64), + Account(AccountId, i64), /// Event log owned by a folder. - Folder(FolderRecord), + Folder(AccountId, FolderRecord), +} + +impl EventLogOwner { + /// Account idenifier. + pub fn account_id(&self) -> &AccountId { + match self { + EventLogOwner::Account(account_id, _) => account_id, + EventLogOwner::Folder(account_id, _) => account_id, + } + } } impl From<&EventLogOwner> for i64 { fn from(value: &EventLogOwner) -> Self { match value { - EventLogOwner::Account(id) => *id, - EventLogOwner::Folder(folder) => folder.row_id, + EventLogOwner::Account(_, id) => *id, + EventLogOwner::Folder(_, folder) => folder.row_id, } } } @@ -143,8 +154,11 @@ where .await?; Ok(match result { - (account_row, None) => EventLogOwner::Account(account_row.row_id), + (account_row, None) => { + EventLogOwner::Account(account_id, account_row.row_id) + } (_, Some(folder_row)) => EventLogOwner::Folder( + account_id, FolderRecord::from_row(folder_row).await?, ), }) @@ -155,6 +169,15 @@ where records: &[EventRecord], delete_before: bool, ) -> Result<(), E> { + if records.is_empty() { + return Ok(()); + } + + let mut span = CommitSpan { + before: self.tree.last_commit(), + after: None, + }; + let log_type = self.log_type.clone(); let mut insert_rows = Vec::new(); let mut commits = Vec::new(); @@ -194,6 +217,14 @@ where self.tree.append(&mut hashes); self.tree.commit(); + span.after = self.tree.last_commit(); + + changes_feed().send_replace(LocalChangeEvent::AccountModified { + account_id: *self.owner.account_id(), + log_type: self.log_type, + commit_span: span, + }); + Ok(()) } } @@ -362,14 +393,19 @@ where })?; for row in rows { + if tx.is_closed() { + break; + } let row = row?; let record: EventRecord = row.try_into()?; - let sender = tx.clone(); - futures::executor::block_on(async move { - if let Err(err) = sender.send(Ok(record)).await { - tracing::error!(error = %err); - } + let inner_tx = tx.clone(); + let res = futures::executor::block_on(async move { + inner_tx.send(Ok(record)).await }); + if let Err(e) = res { + tracing::error!(error = %e); + break; + } } Ok::<_, Error>(()) @@ -630,8 +666,8 @@ where fn version(&self) -> u16 { match &self.owner { - EventLogOwner::Folder(folder) => *folder.summary.version(), - EventLogOwner::Account(_) => VERSION1, + EventLogOwner::Folder(_, folder) => *folder.summary.version(), + EventLogOwner::Account(_, _) => VERSION1, } } } diff --git a/crates/database/src/migrations.rs b/crates/database/src/migrations.rs index 16050d8620..9ac19eb099 100644 --- a/crates/database/src/migrations.rs +++ b/crates/database/src/migrations.rs @@ -13,7 +13,20 @@ mod embedded { pub fn migrate_connection( conn: &mut Connection, ) -> std::result::Result { - embedded::migrations::runner().run(conn) + tracing::debug!("migration::started"); + let report = embedded::migrations::runner().run(conn)?; + let applied = report.applied_migrations(); + for migration in applied { + tracing::debug!( + name = %migration.name(), + version = %migration.version(), + "migration::applied", + ); + } + tracing::debug!( + applied_migrations = %applied.len(), + "migration::finished"); + Ok(report) } /// Run migrations for a client. @@ -22,7 +35,7 @@ pub async fn migrate_client(client: &mut Client) -> Result { oneshot::channel::>(); client .conn_mut(|conn| { - let result = embedded::migrations::runner().run(conn); + let result = migrate_connection(conn); tx.send(result).unwrap(); Ok(()) }) diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index 34c4295bb2..c475450faf 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -114,13 +114,7 @@ async fn import_accounts( let mut client = open_file_with_journal_mode(db_file, JournalMode::Memory).await?; - let report = migrate_client(&mut client).await?; - for migration in report.applied_migrations() { - tracing::debug!( - name = %migration.name(), - version = %migration.version(), - "import_accounts::migration",); - } + migrate_client(&mut client).await?; client } else { open_memory().await? @@ -221,13 +215,6 @@ pub async fn upgrade_accounts( copy_file_blobs(accounts.as_slice(), &options).await?; } - if !options.keep_stale_files { - tracing::debug!("upgrade_accounts::delete_stale_files"); - - result.deleted_files = - delete_stale_files(accounts.as_slice(), &options).await?; - } - result.accounts = accounts; result.database_file = options.paths.database_file().to_owned(); @@ -241,6 +228,13 @@ pub async fn upgrade_accounts( vfs::rename(db_temp.path(), options.paths.database_file()).await?; } + if !options.keep_stale_files { + tracing::debug!("upgrade_accounts::delete_stale_files"); + + result.deleted_files = + delete_stale_files(result.accounts.as_slice(), &options).await?; + } + Ok(result) } @@ -435,6 +429,8 @@ async fn delete_stale_files( let mut files = vec![ options.paths.identity_dir().to_owned(), options.paths.audit_file().to_owned(), + options.paths.global_preferences_file(), + options.paths.local_dir().join(".DS_Store"), ]; for account in accounts { @@ -461,5 +457,12 @@ async fn delete_stale_files( } } + // Can error if the local directory is not empty! + if let Err(error) = vfs::remove_dir(options.paths.local_dir()).await { + tracing::error!( + error = %error, + "upgrade_accounts::remove_local_dir"); + } + Ok(files) } diff --git a/crates/debug_snapshot/Cargo.toml b/crates/debug_snapshot/Cargo.toml new file mode 100644 index 0000000000..e61235e683 --- /dev/null +++ b/crates/debug_snapshot/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sos-debug-snapshot" +version = "0.17.0" +edition = "2021" +description = "Create debug snapshot ZIP archives for the Save Our Secrets SDK" +homepage = "https://saveoursecrets.com" +license = "MIT OR Apache-2.0" +repository = "https://github.com/saveoursecrets/sdk" + +[dependencies] +thiserror.workspace = true +futures.workspace = true +sos-backend.workspace = true +sos-client-storage.workspace = true +sos-archive.workspace = true +sos-logs.workspace = true +sos-sync.workspace = true +sos-vfs.workspace = true +serde_json.workspace = true diff --git a/crates/debug_snapshot/src/error.rs b/crates/debug_snapshot/src/error.rs new file mode 100644 index 0000000000..5d729fe3e7 --- /dev/null +++ b/crates/debug_snapshot/src/error.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +/// Errors generated by the library. +#[derive(Debug, Error)] +pub enum Error { + /// Errors generated by the IO module. + #[error(transparent)] + Io(#[from] std::io::Error), + + /// Errors generated by the JSON library. + #[error(transparent)] + Json(#[from] serde_json::Error), + + /// Errors generated by the backend library. + #[error(transparent)] + Backend(#[from] sos_backend::Error), + + /// Errors generated by the client storage library. + #[error(transparent)] + ClientStorage(#[from] sos_client_storage::Error), + + /// Errors generated by the archive library. + #[error(transparent)] + Archive(#[from] sos_archive::Error), +} diff --git a/crates/debug_snapshot/src/lib.rs b/crates/debug_snapshot/src/lib.rs new file mode 100644 index 0000000000..7bc5e93ef3 --- /dev/null +++ b/crates/debug_snapshot/src/lib.rs @@ -0,0 +1,114 @@ +use futures::{pin_mut, StreamExt}; +use sos_archive::ZipWriter; +use sos_client_storage::{ + ClientBaseStorage, ClientFolderStorage, ClientStorage, +}; +use sos_logs::LOG_FILE_NAME; +use sos_sync::SyncStorage; +use sos_vfs as vfs; +use std::path::Path; + +mod error; +pub use error::Error; + +/// Options for debug snapshots. +#[derive(Debug)] +pub struct DebugSnapshotOptions { + /// Include log files in the archive. + pub include_log_files: bool, + /// Include audit trail for the first configured + /// audit provider. + pub include_audit_trail: bool, +} + +impl Default for DebugSnapshotOptions { + fn default() -> Self { + Self { + include_log_files: true, + include_audit_trail: false, + } + } +} + +/// Export a ZIP archive containing a snapshot of an +/// account state; if the file exists it is overwritten. +/// +/// # Privacy +/// +/// No secret information is included but it does include the +/// account identifier and folder names. +pub async fn export_debug_snapshot( + source: &ClientStorage, + file: impl AsRef, + options: DebugSnapshotOptions, +) -> Result<(), Error> { + let zip_file = vfs::File::create(file.as_ref()).await?; + let mut zip_writer = ZipWriter::new(zip_file); + + let account_id = *source.account_id(); + let debug_tree = source.debug_account_tree(account_id).await?; + + let buffer = serde_json::to_vec_pretty(&debug_tree)?; + zip_writer.add_file("account.json", &buffer).await?; + + let login = source.read_login_vault().await?; + let buffer = serde_json::to_vec_pretty(login.summary())?; + zip_writer.add_file("login.json", &buffer).await?; + + if let Some(device) = source.read_device_vault().await? { + let buffer = serde_json::to_vec_pretty(device.summary())?; + zip_writer.add_file("device.json", &buffer).await?; + } + + let target = source.backend_target(); + let paths = target.paths(); + + if options.include_log_files { + let logs = paths.logs_dir(); + let mut dir = vfs::read_dir(logs).await?; + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + if let Some(name) = path.file_name() { + if name.to_string_lossy().starts_with(LOG_FILE_NAME) { + let buffer = vfs::read(&path).await?; + zip_writer + .add_file( + &format!("logs/{}.jsonl", name.to_string_lossy()), + &buffer, + ) + .await?; + } + } + } + } + + if options.include_audit_trail { + if let Some(providers) = sos_backend::audit::providers() { + for (index, provider) in providers.iter().enumerate() { + let stream = provider.audit_stream(false).await?; + pin_mut!(stream); + + let events = stream + .filter_map(|e| async move { e.ok() }) + .filter_map(|e| async move { + if e.account_id() == &account_id { + Some(e) + } else { + None + } + }) + .collect::>() + .await; + + let buffer = serde_json::to_vec_pretty(&events)?; + zip_writer + .add_file(&format!("audit/{}.json", index), &buffer) + .await?; + } + } + } + + zip_writer.finish().await?; + + Ok(()) +} diff --git a/crates/extension_service/Cargo.toml b/crates/extension_service/Cargo.toml index bcfa18ca3c..cd64832f9b 100644 --- a/crates/extension_service/Cargo.toml +++ b/crates/extension_service/Cargo.toml @@ -11,8 +11,9 @@ repository = "https://github.com/saveoursecrets/sdk" sos-account = { workspace = true, features = ["clipboard", "search"] } sos-backend.workspace = true sos-core.workspace = true -sos-ipc = { workspace = true, features = ["extension-helper-server", "search", "clipboard"] } +sos-ipc = { workspace = true, features = ["extension-helper-server", "search", "clipboard", "files"] } sos-net = { workspace = true, features = ["clipboard", "search"] } +sos-changes = { workspace = true, features = ["changes-consumer"] } tokio.workspace = true anyhow.workspace = true xclipboard.workspace = true diff --git a/crates/extension_service/src/lib.rs b/crates/extension_service/src/lib.rs index 81302020b5..d14c014915 100644 --- a/crates/extension_service/src/lib.rs +++ b/crates/extension_service/src/lib.rs @@ -1,6 +1,6 @@ use sos_account::AccountSwitcherOptions; use sos_backend::{BackendTarget, InferOptions}; -use sos_core::Paths; +use sos_core::{events::changes_feed, Paths}; use sos_ipc::{ extension_helper::server::{ ExtensionHelperOptions, ExtensionHelperServer, @@ -30,6 +30,8 @@ pub async fn run() -> anyhow::Result<()> { let extension_id = args.pop().unwrap_or_else(String::new).to_string(); + let changes_feed = changes_feed(); + let mut accounts = NetworkAccountSwitcher::new_with_options(AccountSwitcherOptions { clipboard: Some(Clipboard::new_timeout(90)?), @@ -64,7 +66,10 @@ pub async fn run() -> anyhow::Result<()> { let accounts = Arc::new(RwLock::new(accounts)); let options = ExtensionHelperOptions::new(extension_id, info); - let server = ExtensionHelperServer::new(options, accounts).await?; + let server = ExtensionHelperServer::new(options, accounts, |event| { + changes_feed.send_replace(event); + }) + .await?; server.listen().await; Ok(()) } diff --git a/crates/filesystem/src/archive/export.rs b/crates/filesystem/src/archive/export.rs index 659c94ea07..06b07e8566 100644 --- a/crates/filesystem/src/archive/export.rs +++ b/crates/filesystem/src/archive/export.rs @@ -2,7 +2,7 @@ use crate::archive::{Error, ManifestVersion1, Result}; use hex; use sha2::{Digest, Sha256}; -use sos_archive::ZipWriter; +use sos_archive::{ZipWriter, ARCHIVE_MANIFEST}; use sos_core::{ constants::{ ACCOUNT_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILE_EVENTS, JSON_EXT, @@ -43,13 +43,13 @@ async fn export_archive_buffer( let vaults = list_local_folders(paths).await?; let mut archive = Vec::new(); - let mut writer = - ZipWriter::new(Cursor::new(&mut archive), ManifestVersion1::new_v2()); - set_identity(&mut writer, address, &identity).await?; + let mut manifest = ManifestVersion1::new_v2(); + let mut writer = ZipWriter::new(Cursor::new(&mut archive)); + set_identity(&mut writer, &mut manifest, address, &identity).await?; for (summary, path) in vaults { let buffer = vfs::read(path).await?; - add_vault(&mut writer, *summary.id(), &buffer).await?; + add_vault(&mut writer, &mut manifest, *summary.id(), &buffer).await?; } let device_info = if vfs::try_exists(paths.device_file()).await? @@ -63,27 +63,37 @@ async fn export_archive_buffer( }; if let Some((vault, events)) = device_info { - add_devices(&mut writer, vault.as_slice(), events.as_slice()).await?; + add_devices( + &mut writer, + &mut manifest, + vault.as_slice(), + events.as_slice(), + ) + .await?; } if vfs::try_exists(paths.account_events()).await? { let buffer = vfs::read(paths.account_events()).await?; - add_account_events(&mut writer, buffer.as_slice()).await?; + add_account_events(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } if vfs::try_exists(paths.file_events()).await? { let buffer = vfs::read(paths.file_events()).await?; - add_file_events(&mut writer, buffer.as_slice()).await?; + add_file_events(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } if vfs::try_exists(paths.preferences_file()).await? { let buffer = vfs::read(paths.preferences_file()).await?; - add_preferences(&mut writer, buffer.as_slice()).await?; + add_preferences(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } if vfs::try_exists(paths.remote_origins()).await? { let buffer = vfs::read(paths.remote_origins()).await?; - add_remote_servers(&mut writer, buffer.as_slice()).await?; + add_remote_servers(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } let external_files = @@ -96,22 +106,27 @@ async fn export_archive_buffer( .await?; } + let manifest = serde_json::to_vec_pretty(&manifest)?; + writer + .add_file(ARCHIVE_MANIFEST, manifest.as_slice()) + .await?; + writer.finish().await?; Ok(archive) } /// Set the identity vault for the archive. async fn set_identity( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, account_id: &AccountId, vault: &[u8], ) -> Result<()> { let mut path = PathBuf::from(account_id.to_string()); path.set_extension(VAULT_EXT); - writer.manifest_mut().account_id = *account_id; - writer.manifest_mut().checksum = - hex::encode(Sha256::digest(vault).as_slice()); + manifest.account_id = *account_id; + manifest.checksum = hex::encode(Sha256::digest(vault).as_slice()); writer .add_file(path.to_string_lossy().as_ref(), vault) .await?; @@ -121,7 +136,8 @@ async fn set_identity( /// Add a vault to the archive. async fn add_vault( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, vault_id: VaultId, vault: &[u8], ) -> Result<()> { @@ -129,7 +145,7 @@ async fn add_vault( path.set_extension(VAULT_EXT); let checksum = hex::encode(Sha256::digest(vault).as_slice()); - writer.manifest_mut().vaults.insert(vault_id, checksum); + manifest.vaults.insert(vault_id, checksum); writer .add_file(path.to_string_lossy().as_ref(), vault) .await?; @@ -139,13 +155,14 @@ async fn add_vault( /// Add a devices vault to the archive. async fn add_devices( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, vault: &[u8], events: &[u8], ) -> Result<()> { let vault_checksum = hex::encode(Sha256::digest(vault).as_slice()); let event_checksum = hex::encode(Sha256::digest(events).as_slice()); - writer.manifest_mut().devices = Some((vault_checksum, event_checksum)); + manifest.devices = Some((vault_checksum, event_checksum)); // Create the device vault file let mut path = PathBuf::from(DEVICE_FILE); @@ -166,11 +183,12 @@ async fn add_devices( /// Add account events to the archive. async fn add_account_events( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, events: &[u8], ) -> Result<()> { let event_checksum = hex::encode(Sha256::digest(events).as_slice()); - writer.manifest_mut().account = Some(event_checksum); + manifest.account = Some(event_checksum); // Create the account events file let mut path = PathBuf::from(ACCOUNT_EVENTS); @@ -184,11 +202,12 @@ async fn add_account_events( /// Add file events to the archive. async fn add_file_events( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, events: &[u8], ) -> Result<()> { let event_checksum = hex::encode(Sha256::digest(events).as_slice()); - writer.manifest_mut().files = Some(event_checksum); + manifest.files = Some(event_checksum); // Create the file events file let mut path = PathBuf::from(FILE_EVENTS); @@ -202,11 +221,12 @@ async fn add_file_events( /// Add account-specific preferences. async fn add_preferences( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, prefs: &[u8], ) -> Result<()> { let checksum = hex::encode(Sha256::digest(prefs).as_slice()); - writer.manifest_mut().preferences = Some(checksum); + manifest.preferences = Some(checksum); // Create the file events file let mut path = PathBuf::from(PREFERENCES_FILE); @@ -221,11 +241,12 @@ async fn add_preferences( /// Add remote server settings. async fn add_remote_servers( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, remotes: &[u8], ) -> Result<()> { let checksum = hex::encode(Sha256::digest(remotes).as_slice()); - writer.manifest_mut().remotes = Some(checksum); + manifest.remotes = Some(checksum); // Create the file events file let mut path = PathBuf::from(REMOTES_FILE); diff --git a/crates/filesystem/src/archive/import.rs b/crates/filesystem/src/archive/import.rs index 616c80c096..85ced1333c 100644 --- a/crates/filesystem/src/archive/import.rs +++ b/crates/filesystem/src/archive/import.rs @@ -6,6 +6,8 @@ use crate::{write_exclusive, FolderEventLog, VaultFileWriter}; use hex; use sha2::{Digest, Sha256}; use sos_archive::{sanitize_file_path, ZipReader}; +use sos_core::events::EventLogType; +use sos_core::AccountId; use sos_core::{ constants::{ ACCOUNT_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR, FILE_EVENTS, @@ -55,8 +57,8 @@ async fn import_archive_reader( )); } - let address_path = restore_targets.manifest.account_id; - let paths = paths.with_account_id(&address_path); + let account_id = restore_targets.manifest.account_id; + let paths = paths.with_account_id(&account_id); // Write out the identity vault let identity_vault: Vault = decode(&restore_targets.identity.1).await?; @@ -65,8 +67,12 @@ async fn import_archive_reader( // Write out the identity event log file let (_, events) = FolderReducer::split::(identity_vault).await?; - let mut event_log = - FolderEventLog::::new_folder(paths.identity_events()).await?; + let mut event_log = FolderEventLog::::new_folder( + paths.identity_events(), + account_id, + EventLogType::Identity, + ) + .await?; event_log.apply(events.as_slice()).await?; // Check if the identity name already exists @@ -97,7 +103,8 @@ async fn import_archive_reader( vfs::create_dir_all(&vaults_dir).await?; restore_system(&paths, &restore_targets).await?; - restore_user_folders(&paths, &restore_targets.vaults).await?; + restore_user_folders(&paths, &account_id, &restore_targets.vaults) + .await?; let account = PublicIdentity::new(restore_targets.manifest.account_id, label); Ok(account) @@ -105,12 +112,14 @@ async fn import_archive_reader( async fn restore_user_folders( paths: &Paths, + account_id: &AccountId, vaults: &Vec<(Vec, Vault)>, ) -> Result<()> { // Write out each vault and the event log for (buffer, vault) in vaults { - let vault_path = paths.vault_path(vault.id()); - let event_log_path = paths.event_log_path(vault.id()); + let folder_id = *vault.id(); + let vault_path = paths.vault_path(&folder_id); + let event_log_path = paths.event_log_path(&folder_id); // Write out the vault buffer write_exclusive(&vault_path, buffer).await?; @@ -119,8 +128,12 @@ async fn restore_user_folders( FolderReducer::split::(vault.clone()).await?; // Write out the event log file - let mut event_log = - FolderEventLog::::new_folder(event_log_path).await?; + let mut event_log = FolderEventLog::::new_folder( + event_log_path, + *account_id, + EventLogType::Folder(folder_id), + ) + .await?; event_log.apply(events.as_slice()).await?; } diff --git a/crates/filesystem/src/audit_provider.rs b/crates/filesystem/src/audit_provider.rs index f7b5b7f752..4424a09dad 100644 --- a/crates/filesystem/src/audit_provider.rs +++ b/crates/filesystem/src/audit_provider.rs @@ -222,6 +222,9 @@ where let it_file = self.file.clone(); tokio::task::spawn(async move { while let Some(record) = it.next().await? { + if tx.is_closed() { + break; + } let mut inner = it_file.lock().await; let event = inner.read_event(&record).await?; if let Err(e) = tx.send(Ok(event)).await { diff --git a/crates/filesystem/src/event_log.rs b/crates/filesystem/src/event_log.rs index c371450ebf..495ca0ef17 100644 --- a/crates/filesystem/src/event_log.rs +++ b/crates/filesystem/src/event_log.rs @@ -25,13 +25,16 @@ use async_trait::async_trait; use binary_stream::futures::{BinaryReader, Decodable, Encodable}; use futures::{stream::BoxStream, StreamExt, TryStreamExt}; use sos_core::{ - commit::{CommitHash, CommitProof, CommitTree, Comparison}, + commit::{CommitHash, CommitProof, CommitSpan, CommitTree, Comparison}, encode, encoding::{encoding_options, VERSION1}, events::{ + changes_feed, patch::{CheckedPatch, Diff, Patch}, - AccountEvent, DeviceEvent, EventRecord, WriteEvent, + AccountEvent, DeviceEvent, EventLogType, EventRecord, + LocalChangeEvent, WriteEvent, }, + AccountId, }; use sos_vfs::{self as vfs, File, OpenOptions}; use std::result::Result as StdResult; @@ -103,6 +106,8 @@ where + Sync + 'static, { + account_id: AccountId, + log_type: EventLogType, tree: CommitTree, data: PathBuf, identity: &'static [u8], @@ -137,6 +142,9 @@ where let file_path = self.data.clone(); tokio::task::spawn(async move { while let Some(record) = it.next().await? { + if tx.is_closed() { + break; + } let event_buffer = read_event_buffer(file_path.clone(), &record).await?; let event_record = record.into_event_record(event_buffer); @@ -291,6 +299,15 @@ where &mut self, records: Vec, ) -> StdResult<(), Self::Error> { + if records.is_empty() { + return Ok(()); + } + + let mut span = CommitSpan { + before: self.tree.last_commit(), + after: None, + }; + let mut buffer: Vec = Vec::new(); let mut commits = Vec::new(); let mut last_commit_hash = self.tree().last_commit(); @@ -328,6 +345,17 @@ where commits.iter().map(|c| *c.as_ref()).collect::>(); self.tree.append(&mut hashes); self.tree.commit(); + + span.after = self.tree.last_commit(); + + changes_feed().send_replace( + LocalChangeEvent::AccountModified { + account_id: self.account_id, + log_type: self.log_type, + commit_span: span, + }, + ); + Ok(()) } Err(e) => Err(e.into()), @@ -662,7 +690,11 @@ where + 'static, { /// Create a new folder event log file. - pub async fn new_folder>(path: P) -> StdResult { + pub async fn new_folder>( + path: P, + account_id: AccountId, + log_type: EventLogType, + ) -> StdResult { use sos_core::constants::FOLDER_EVENT_LOG_IDENTITY; // Note that for backwards compatibility we don't // encode a version, later we will need to upgrade @@ -680,6 +712,8 @@ where Ok(Self { data: path.as_ref().to_path_buf(), tree: Default::default(), + log_type, + account_id, identity: &FOLDER_EVENT_LOG_IDENTITY, version: None, phantom: std::marker::PhantomData, @@ -699,7 +733,10 @@ where + 'static, { /// Create a new account event log file. - pub async fn new_account>(path: P) -> StdResult { + pub async fn new_account>( + path: P, + account_id: AccountId, + ) -> StdResult { use sos_core::{ constants::ACCOUNT_EVENT_LOG_IDENTITY, encoding::VERSION, }; @@ -714,6 +751,8 @@ where .await?; Ok(Self { + account_id, + log_type: EventLogType::Account, data: path.as_ref().to_path_buf(), tree: Default::default(), identity: &ACCOUNT_EVENT_LOG_IDENTITY, @@ -735,7 +774,10 @@ where + 'static, { /// Create a new device event log file. - pub async fn new_device(path: impl AsRef) -> StdResult { + pub async fn new_device( + path: impl AsRef, + account_id: AccountId, + ) -> StdResult { use sos_core::{ constants::DEVICE_EVENT_LOG_IDENTITY, encoding::VERSION, }; @@ -751,6 +793,8 @@ where .await?; Ok(Self { + log_type: EventLogType::Device, + account_id, data: path.as_ref().to_path_buf(), tree: Default::default(), identity: &DEVICE_EVENT_LOG_IDENTITY, @@ -773,7 +817,10 @@ where + 'static, { /// Create a new file event log file. - pub async fn new_file(path: impl AsRef) -> StdResult { + pub async fn new_file( + path: impl AsRef, + account_id: AccountId, + ) -> StdResult { use sos_core::{ constants::FILE_EVENT_LOG_IDENTITY, encoding::VERSION, }; @@ -789,6 +836,8 @@ where .await?; Ok(Self { + account_id, + log_type: EventLogType::Files, data: path.as_ref().to_path_buf(), tree: Default::default(), identity: &FILE_EVENT_LOG_IDENTITY, diff --git a/crates/ipc/Cargo.toml b/crates/ipc/Cargo.toml index 69008fda30..032d38a522 100644 --- a/crates/ipc/Cargo.toml +++ b/crates/ipc/Cargo.toml @@ -16,6 +16,7 @@ local-transport = [ "serde_with", "async-trait", ] + memory-http-server = [] clipboard = ["sos-account/clipboard"] contacts = ["sos-protocol/contacts"] @@ -57,6 +58,7 @@ extension-helper-client = [ [dependencies] sos-account.workspace = true sos-core.workspace = true +sos-changes.workspace = true sos-client-storage.workspace = true sos-backend.workspace = true sos-database.workspace = true diff --git a/crates/ipc/src/error.rs b/crates/ipc/src/error.rs index 598003bf10..6c681c543a 100644 --- a/crates/ipc/src/error.rs +++ b/crates/ipc/src/error.rs @@ -75,6 +75,10 @@ pub enum Error { #[error(transparent)] Network(#[from] sos_protocol::NetworkError), + /// Errors generated by the changes library. + #[error(transparent)] + Changes(#[from] sos_changes::Error), + /// Errors generated on conflict. #[error(transparent)] Conflict(#[from] sos_protocol::ConflictError), diff --git a/crates/ipc/src/extension_helper/server.rs b/crates/ipc/src/extension_helper/server.rs index a5e27e4ca7..b47a7b1fcd 100644 --- a/crates/ipc/src/extension_helper/server.rs +++ b/crates/ipc/src/extension_helper/server.rs @@ -12,7 +12,8 @@ use http::{ StatusCode, }; use sos_account::{Account, AccountSwitcher}; -use sos_core::ErrorExt; +use sos_changes::consumer::ChangeConsumer; +use sos_core::{events::LocalChangeEvent, ErrorExt, Paths}; use sos_login::DelegatedAccess; use sos_logs::Logger; use sos_protocol::{constants::MIME_TYPE_JSON, ErrorReply}; @@ -98,6 +99,7 @@ where pub async fn new( options: ExtensionHelperOptions, accounts: Arc>>, + change_handler: impl Fn(LocalChangeEvent) + Send + Sync + 'static, ) -> Result { let log_level = std::env::var("SOS_NATIVE_BRIDGE_LOG_LEVEL") .map(|s| s.to_string()) @@ -113,8 +115,15 @@ where } tracing::info!(options = ?options, "extension_helper"); - + let paths = { + let accounts = accounts.read().await; + accounts.paths().unwrap_or_else(|| { + Paths::new_client(Paths::data_dir().unwrap()) + }) + }; let accounts = WebAccounts::new(accounts); + let changes_consumer = ChangeConsumer::listen(paths.clone())?; + accounts.listen_changes(changes_consumer, paths, change_handler)?; let client = LocalMemoryServer::listen( accounts.clone(), options.service_info.clone(), diff --git a/crates/ipc/src/web_service/account.rs b/crates/ipc/src/web_service/account.rs index 77590b2f28..13f0e6f1ee 100644 --- a/crates/ipc/src/web_service/account.rs +++ b/crates/ipc/src/web_service/account.rs @@ -232,7 +232,7 @@ where /// Sign in to an account pub async fn sign_in_password( - accounts: WebAccounts, + mut accounts: WebAccounts, account_id: AccountId, password: SecretString, save_password: bool, @@ -257,52 +257,43 @@ where + 'static, { use sos_platform_authenticator::keyring_password; + { + let mut user_accounts = accounts.as_ref().write().await; + let Some(account) = user_accounts + .iter_mut() + .find(|a| a.account_id() == &account_id) + else { + return status(StatusCode::NOT_FOUND); + }; - let mut user_accounts = accounts.as_ref().write().await; - let Some(account) = user_accounts - .iter_mut() - .find(|a| a.account_id() == &account_id) - else { - return status(StatusCode::NOT_FOUND); - }; - - let key: AccessKey = password.clone().into(); - - let folder_ids = if let Ok(folders) = account.list_folders().await { - folders.into_iter().map(|f| *f.id()).collect::>() - } else { - vec![] - }; + let key: AccessKey = password.clone().into(); - match account.sign_in(&key).await { - Ok(_) => { - if let Err(e) = - accounts.watch(account_id, account.paths(), folder_ids) - { - tracing::error!(error = ?e); + match account.sign_in(&key).await { + Ok(_) => {} + Err(e) => { + if e.is_permission_denied() { + return status(StatusCode::FORBIDDEN); + } else { + return internal_server_error(e); + } } } - Err(e) => { - if e.is_permission_denied() { - return status(StatusCode::FORBIDDEN); - } else { + + if let Err(e) = account.initialize_search_index().await { + return internal_server_error(e); + } + + if save_password && keyring_password::supported() { + if let Err(e) = keyring_password::save_account_password( + &account_id.to_string(), + password, + ) { return internal_server_error(e); } } } - if let Err(e) = account.initialize_search_index().await { - return internal_server_error(e); - } - - if save_password && keyring_password::supported() { - if let Err(e) = keyring_password::save_account_password( - &account_id.to_string(), - password, - ) { - return internal_server_error(e); - } - } + accounts.watch(account_id); status(StatusCode::OK) } @@ -373,7 +364,7 @@ where /// Sign out of an account pub async fn sign_out( - accounts: WebAccounts, + mut accounts: WebAccounts, account_id: Option, ) -> hyper::Result> where @@ -395,61 +386,55 @@ where + Sync + 'static, { - let mut user_accounts = accounts.as_ref().write().await; if let Some(account_id) = account_id { - let Some(account) = user_accounts - .iter_mut() - .find(|a| a.account_id() == &account_id) - else { - return status(StatusCode::NOT_FOUND); - }; - - let folder_ids = if let Ok(folders) = account.list_folders().await { - folders.into_iter().map(|f| *f.id()).collect::>() - } else { - vec![] - }; + { + let mut user_accounts = accounts.as_ref().write().await; + let Some(account) = user_accounts + .iter_mut() + .find(|a| a.account_id() == &account_id) + else { + return status(StatusCode::NOT_FOUND); + }; - match account.sign_out().await { - Ok(_) => { - if let Err(e) = - accounts.unwatch(&account_id, account.paths(), folder_ids) - { - return internal_server_error(e); - } - status(StatusCode::OK) + match account.sign_out().await { + Ok(_) => {} + Err(e) => return internal_server_error(e), } - Err(e) => internal_server_error(e), } + + accounts.unwatch(&account_id); + status(StatusCode::OK) } else { - let mut account_info = Vec::new(); - for account in user_accounts.iter() { - let folder_ids = if let Ok(folders) = account.list_folders().await - { - folders.into_iter().map(|f| *f.id()).collect::>() - } else { - vec![] - }; + let account_info = { + let mut user_accounts = accounts.as_ref().write().await; + let mut account_info = Vec::new(); + for account in user_accounts.iter() { + let folder_ids = if let Ok(folders) = + account.list_folders().await + { + folders.into_iter().map(|f| *f.id()).collect::>() + } else { + vec![] + }; - account_info.push(( - *account.account_id(), - account.paths(), - folder_ids, - )); - } + account_info.push(( + *account.account_id(), + account.paths(), + folder_ids, + )); + } - match user_accounts.sign_out_all().await { - Ok(_) => { - for (account_id, paths, folder_ids) in account_info { - if let Err(e) = - accounts.unwatch(&account_id, paths, folder_ids) - { - return internal_server_error(e); - } - } - status(StatusCode::OK) + match user_accounts.sign_out_all().await { + Ok(_) => {} + Err(e) => return internal_server_error(e), } - Err(e) => internal_server_error(e), + account_info + }; + + for (account_id, _, _) in account_info { + accounts.unwatch(&account_id); } + + status(StatusCode::OK) } } diff --git a/crates/ipc/src/web_service/mod.rs b/crates/ipc/src/web_service/mod.rs index 695f86be39..7221cc7cde 100644 --- a/crates/ipc/src/web_service/mod.rs +++ b/crates/ipc/src/web_service/mod.rs @@ -28,14 +28,16 @@ mod common; mod helpers; mod search; mod secret; -mod web_accounts; +// mod web_accounts; +mod web_accounts2; use account::*; use common::*; use helpers::*; use search::*; use secret::*; -pub use web_accounts::*; +// pub use web_accounts::*; +pub use web_accounts2::*; async fn index( app_info: Arc, diff --git a/crates/ipc/src/web_service/web_accounts2.rs b/crates/ipc/src/web_service/web_accounts2.rs new file mode 100644 index 0000000000..81589031e8 --- /dev/null +++ b/crates/ipc/src/web_service/web_accounts2.rs @@ -0,0 +1,578 @@ +use crate::{Error, FileEventError, Result}; +use serde::{Deserialize, Serialize}; +use sos_account::{Account, AccountSwitcher}; +use sos_backend::BackendTarget; +use sos_changes::consumer::ConsumerHandle; +use sos_core::{ + events::{ + AccountEvent, EventLog, EventLogType, LocalChangeEvent, WriteEvent, + }, + AccountId, ErrorExt, Paths, VaultId, +}; +use sos_login::DelegatedAccess; +use sos_sync::SyncStorage; +use sos_vault::SecretAccess; +use std::{collections::HashSet, sync::Arc}; +use tokio::sync::{broadcast, RwLock}; + +/// Event broadcast when an account changes. +#[typeshare::typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountChangeEvent { + /// Account identifier. + pub account_id: AccountId, + /// Event records with information about the changes. + pub records: ChangeRecords, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ChangeRecords { + /// Account level events. + Account(Vec), + /// Folder level events. + Folder(VaultId, Vec), +} + +impl ChangeRecords { + /// Determine if the records are empty. + pub fn is_empty(&self) -> bool { + match self { + Self::Account(records) => records.is_empty(), + Self::Folder(_, records) => records.is_empty(), + } + } +} + +/// User accounts for the web service. +pub struct WebAccounts +where + A: Account + SyncStorage, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + accounts: Arc>>, + watched_accounts: HashSet, + channel: broadcast::Sender, +} + +impl Clone for WebAccounts +where + A: Account + SyncStorage, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + fn clone(&self) -> Self { + Self { + accounts: self.accounts.clone(), + watched_accounts: self.watched_accounts.clone(), + channel: self.channel.clone(), + } + } +} + +impl WebAccounts +where + A: Account + + SyncStorage + + DelegatedAccess, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + /// Create new accounts. + pub fn new(accounts: Arc>>) -> Self { + let (tx, _) = broadcast::channel::(64); + Self { + accounts, + watched_accounts: HashSet::new(), + channel: tx, + } + } + + /// Create a backend target for the accounts. + pub async fn backend_target(&self) -> Result { + let accounts = self.accounts.read().await; + let paths = if let Some(paths) = accounts.paths() { + paths + } else { + Paths::new_client(Paths::data_dir().unwrap()) + }; + Ok(BackendTarget::from_paths(&paths).await?) + } + + /// Subscribe to change events. + pub fn subscribe(&self) -> broadcast::Receiver { + self.channel.subscribe() + } + + /// Start listening for changes. + pub fn listen_changes( + &self, + mut changes_consumer: ConsumerHandle, + paths: Arc, + change_handler: impl Fn(LocalChangeEvent) + Send + Sync + 'static, + ) -> Result<()> { + // Start a background task to listen for change events + let channel = self.channel.clone(); + let task_accounts = self.accounts.clone(); + + tokio::task::spawn(async move { + let receiver = changes_consumer.changes(); + + while let Some(event) = receiver.recv().await { + tracing::debug!( + event = ?event, + "change_consumer::event_received" + ); + + if let Err(e) = process_change_event( + &event, + AccountId::default(), + paths.clone(), + task_accounts.clone(), + channel.clone(), + ) + .await + { + tracing::error!(error = %e, "process_change_event"); + } + + change_handler(event); + } + + tracing::debug!("consumer_task_completed"); + }); + + Ok(()) + } + + /// Start watching an account for changes. + pub fn watch(&mut self, account_id: AccountId) { + self.watched_accounts.insert(account_id); + } + + /// Stop watching for changes. + pub fn unwatch(&mut self, account_id: &AccountId) { + self.watched_accounts.remove(account_id); + } +} + +impl AsRef>>> + for WebAccounts +where + A: Account + SyncStorage, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + fn as_ref(&self) -> &Arc>> { + &self.accounts + } +} + +/// Process change events and update the system state accordingly +async fn process_change_event( + event: &LocalChangeEvent, + _account_id: AccountId, + _paths: Arc, + accounts: Arc>>, + channel: broadcast::Sender, +) -> Result<()> +where + A: Account + + SyncStorage + + DelegatedAccess + + Sync + + Send + + 'static, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + match event { + LocalChangeEvent::Init => { + // Just initialization, nothing to do + Ok(()) + } + LocalChangeEvent::AccountCreated(account_id) => { + // New account created + // NOTE: This would need implementation if we want to handle new accounts + tracing::debug!(account_id = %account_id, "account_created_event"); + // TODO: Implement account creation handling if needed + Ok(()) + } + LocalChangeEvent::AccountDeleted(account_id) => { + // Account deleted + tracing::debug!(account_id = %account_id, "account_deleted_event"); + // TODO: Implement account deletion handling if needed + Ok(()) + } + LocalChangeEvent::AccountModified { + account_id, + log_type, + commit_span: _, + } => { + // Account was modified - process changes based on log_type + tracing::debug!(account_id = %account_id, log_type = ?log_type, "account_modified_event"); + + match log_type { + EventLogType::Identity + | EventLogType::Account + | EventLogType::Device => { + // Account-level changes + let mut accounts_lock = accounts.write().await; + let account = accounts_lock + .iter_mut() + .find(|a| a.account_id() == account_id) + .ok_or(Error::from(FileEventError::NoAccount( + account_id.clone(), + )))?; + + // Reload the identity folder for account-level changes + account.reload_login_folder().await.map_err(|e| { + Error::from(FileEventError::ReloadIdentityFolder( + e.to_string(), + )) + })?; + + // Load account events + let records = load_account_records(account).await?; + + // Update folders in memory + tracing::debug!("account_change::load_folders"); + if let Err(e) = account.load_folders().await { + tracing::error!(error = %e, "load_folders_error"); + } + + // Update search index + let records_clone = + ChangeRecords::Account(records.clone()); + update_account_search_index(account, &records_clone) + .await + .map_err(|e| { + Error::from(FileEventError::UpdateSearchIndex( + e.to_string(), + )) + })?; + + // Send event if there are records + if !records.is_empty() { + let evt = AccountChangeEvent { + account_id: account_id.clone(), + records: ChangeRecords::Account(records), + }; + if let Err(e) = channel.send(evt) { + tracing::error!(error = ?e, "account_channel::send"); + } + } + Ok(()) + } + EventLogType::Folder(folder_id) => { + // Folder-level changes + let accounts_lock = accounts.read().await; + let account = accounts_lock + .iter() + .find(|a| a.account_id() == account_id) + .ok_or(Error::from(FileEventError::NoAccount( + account_id.clone(), + )))?; + + let folder = + account.folder(&folder_id).await.ok().ok_or( + Error::from(FileEventError::NoFolder(*folder_id)), + )?; + + let event_log = folder.event_log(); + let mut event_log = event_log.write().await; + let commit = event_log.tree().last_commit(); + let patch = + event_log.diff_events(commit.as_ref()).await?; + let records = patch.into_events::().await?; + + event_log.load_tree().await?; + + // Update search index for folder changes + { + let mut accounts_lock = accounts.write().await; + if let Some(account) = accounts_lock + .iter_mut() + .find(|a| a.account_id() == account_id) + { + let records_clone = ChangeRecords::Folder( + *folder_id, + records.clone(), + ); + update_account_search_index( + account, + &records_clone, + ) + .await + .map_err(|e| { + Error::from( + FileEventError::UpdateSearchIndex( + e.to_string(), + ), + ) + })?; + } + } + + // Send event if there are records + if !records.is_empty() { + let evt = AccountChangeEvent { + account_id: account_id.clone(), + records: ChangeRecords::Folder( + *folder_id, records, + ), + }; + if let Err(e) = channel.send(evt) { + tracing::error!(error = ?e, "account_channel::send"); + } + } + + Ok(()) + } + #[cfg(feature = "files")] + EventLogType::Files => { + // No need to handle file change events + Ok(()) + } + } + } + } +} + +/// Update the search index for an account. +async fn update_account_search_index( + account: &mut A, + records: &ChangeRecords, +) -> std::result::Result<(), E> +where + A: Account + + SyncStorage + + DelegatedAccess, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + let paths = account.paths(); + let index = account.search_index().await?; + + let folder_ids = match records { + ChangeRecords::Account(events) => { + let mut folder_ids = Vec::new(); + for event in events { + match event { + AccountEvent::CreateFolder(folder_id, _) => { + folder_ids.push(*folder_id); + } + AccountEvent::DeleteFolder(folder_id) => { + folder_ids.push(*folder_id) + } + _ => {} + } + } + folder_ids + } + ChangeRecords::Folder(folder_id, _) => vec![*folder_id], + }; + + for folder_id in folder_ids { + match records { + ChangeRecords::Account(events) => { + for event in events { + match event { + AccountEvent::CreateFolder(_, _) => { + // Find the folder password which should be available + // as the identity folder has been reloaded already + let key = account + .find_folder_password(&folder_id) + .await? + .ok_or( + sos_account::Error::NoFolderPassword( + folder_id, + ), + )?; + // Import the vault into the account + account + .import_folder( + paths.vault_path(&folder_id), + key, + true, + ) + .await?; + + // Now the storage should have the folder so + // we can access the access point and add it to + // the search index + if let Some(folder) = + account.folder(&folder_id).await.ok() + { + let access_point = folder.access_point(); + let access_point = access_point.lock().await; + let mut index = index.write().await; + index.add_folder(&*access_point).await?; + } + } + AccountEvent::DeleteFolder(_) => { + let mut index = index.write().await; + index.remove_vault(&folder_id); + } + _ => {} + } + } + } + ChangeRecords::Folder(folder_id, events) => { + if let Some(folder) = account.folder(&folder_id).await.ok() { + let access_point = folder.access_point(); + let mut access_point = access_point.lock().await; + + // Must reload the vault before updating the + // search index + let path = paths.vault_path(folder_id); + access_point.reload_vault(path).await?; + + for event in events { + match event { + WriteEvent::CreateSecret(secret_id, _) => { + if let Some((meta, secret, _)) = access_point + .read_secret(secret_id) + .await? + { + let mut index = index.write().await; + index.add( + folder_id, secret_id, &meta, &secret, + ); + } + } + WriteEvent::UpdateSecret(secret_id, _) => { + if let Some((meta, secret, _)) = access_point + .read_secret(secret_id) + .await? + { + let mut index = index.write().await; + index.update( + folder_id, secret_id, &meta, &secret, + ); + } + } + WriteEvent::DeleteSecret(secret_id) => { + let mut index = index.write().await; + index.remove(folder_id, secret_id); + } + _ => {} + } + } + } + } + } + } + + Ok(()) +} + +async fn load_account_records( + account: &A, +) -> Result> +where + A: Account + + SyncStorage + + Sync + + Send + + 'static, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + // FIXME: update the error handling to avoid the unwrap + let account_log = account.account_log().await.unwrap(); + let mut event_log = account_log.write().await; + let commit = event_log.tree().last_commit(); + + let patch = event_log.diff_events(commit.as_ref()).await?; + let records = patch.into_events::().await?; + + event_log.load_tree().await?; + Ok(records) +} diff --git a/crates/login/src/identity_folder.rs b/crates/login/src/identity_folder.rs index 51a4549dd4..8f9f0ca334 100644 --- a/crates/login/src/identity_folder.rs +++ b/crates/login/src/identity_folder.rs @@ -14,8 +14,9 @@ use sos_backend::{ AccessPoint, BackendTarget, Folder, FolderEventLog, }; use sos_core::{ - constants::LOGIN_AGE_KEY_URN, crypto::AccessKey, encode, AccountId, - AuthenticationError, VaultFlags, VaultId, + constants::LOGIN_AGE_KEY_URN, crypto::AccessKey, encode, + events::EventLogType, AccountId, AuthenticationError, VaultFlags, + VaultId, }; use sos_filesystem::write_exclusive; use sos_vault::Summary; @@ -66,7 +67,12 @@ impl IdentityFolder { BackendTarget::FileSystem(paths) => { let buffer = encode(&vault).await?; write_exclusive(paths.identity_vault(), buffer).await?; - Folder::from_path(paths.identity_vault()).await? + Folder::from_path( + paths.identity_vault(), + &account_id, + EventLogType::Identity, + ) + .await? } BackendTarget::Database(_, client) => { let account_row = AccountRow::new_insert(&account_id, name)?; @@ -466,7 +472,12 @@ impl IdentityFolder { let target = target.clone().with_account_id(account_id); let mut folder = match &target { BackendTarget::FileSystem(paths) => { - Folder::from_path(paths.identity_vault()).await? + Folder::from_path( + paths.identity_vault(), + account_id, + EventLogType::Identity, + ) + .await? } BackendTarget::Database(_, client) => { let (_, login_folder) = diff --git a/crates/logs/src/lib.rs b/crates/logs/src/lib.rs index 3f851d6335..91895260b3 100644 --- a/crates/logs/src/lib.rs +++ b/crates/logs/src/lib.rs @@ -6,6 +6,6 @@ mod error; mod logger; pub use error::Error; -pub use logger::{LogFileStatus, Logger}; +pub use logger::{LogFileStatus, Logger, LOG_FILE_NAME}; pub(crate) type Result = std::result::Result; diff --git a/crates/logs/src/logger.rs b/crates/logs/src/logger.rs index a8fc45807a..9f87942631 100644 --- a/crates/logs/src/logger.rs +++ b/crates/logs/src/logger.rs @@ -12,9 +12,12 @@ use time::OffsetDateTime; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -const LOG_FILE_NAME: &str = "saveoursecrets.log"; +/// File name prefix for log files. +#[doc(hidden)] +pub const LOG_FILE_NAME: &str = "saveoursecrets.log"; + const DEFAULT_LOG_LEVEL: &str = - "sos=info,sos_sdk=debug,sos_net=debug,sos_bindings=debug"; + "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug,sos_app=debug,sos_database_upgrader=debug"; /// State of the log files on disc. pub struct LogFileStatus { diff --git a/crates/net/src/account/file_transfers/mod.rs b/crates/net/src/account/file_transfers/mod.rs index 1fbeb30f01..d361bc7c7f 100644 --- a/crates/net/src/account/file_transfers/mod.rs +++ b/crates/net/src/account/file_transfers/mod.rs @@ -194,21 +194,27 @@ impl FileTransfersHandle { /// Send a collection of items to be added to the queue. pub async fn send(&self, items: FileTransferQueueRequest) { - let res = self.queue_tx.send(items).await; - if let Err(error) = res { - tracing::warn!(error = ?error); + if let Err(error) = self.queue_tx.send(items).await { + tracing::warn!( + error = ?error, + "file_transfers::queue_send_error", + ); } } /// Shutdown the file transfers loop. pub async fn shutdown(self) { - let res = self.shutdown_tx.send(()).await; - if let Err(error) = res { - tracing::warn!(error = ?error); + if let Err(error) = self.shutdown_tx.send(()).await { + tracing::warn!( + error = ?error, + "file_transfers::shutdown_tx::send_error", + ); } - let res = self.shutdown_rx.await; - if let Err(error) = res { - tracing::warn!(error = ?error); + if let Err(error) = self.shutdown_rx.await { + tracing::warn!( + error = ?error, + "file_transfers::shutdown_tx::recv_error", + ); } } } diff --git a/crates/net/src/account/listen.rs b/crates/net/src/account/listen.rs index 5aee907df9..89b5a6c3dc 100644 --- a/crates/net/src/account/listen.rs +++ b/crates/net/src/account/listen.rs @@ -3,7 +3,7 @@ use crate::{Error, NetworkAccount, Result}; use sos_core::Origin; use sos_protocol::{ - network_client::ListenOptions, ChangeNotification, RemoteResult, + network_client::ListenOptions, NetworkChangeEvent, RemoteResult, RemoteSync, }; use sos_sync::SyncStorage; @@ -40,7 +40,7 @@ impl NetworkAccount { origin: &Origin, options: ListenOptions, listener: Option< - mpsc::Sender<(ChangeNotification, RemoteResult)>, + mpsc::Sender<(NetworkChangeEvent, RemoteResult)>, >, ) -> Result<()> { let remotes = self.remotes.read().await; @@ -48,7 +48,7 @@ impl NetworkAccount { self.stop_listening(&origin).await; let remote = Arc::new(remote.clone()); - let (tx, mut rx) = mpsc::channel::(32); + let (tx, mut rx) = mpsc::channel::(32); let local_account = Arc::clone(&self.account); let sync_lock = Arc::clone(&self.sync_lock); diff --git a/crates/net/src/account/remote.rs b/crates/net/src/account/remote.rs index 92c5f48510..b424f2a4df 100644 --- a/crates/net/src/account/remote.rs +++ b/crates/net/src/account/remote.rs @@ -204,7 +204,7 @@ mod listen { use crate::RemoteBridge; use sos_protocol::{ network_client::{ListenOptions, WebSocketHandle}, - ChangeNotification, + NetworkChangeEvent, }; use tokio::sync::mpsc; @@ -223,7 +223,7 @@ mod listen { pub(crate) fn listen( &self, options: ListenOptions, - channel: mpsc::Sender, + channel: mpsc::Sender, ) -> WebSocketHandle { let handle = self.client.listen(options, move |notification| { let tx = channel.clone(); diff --git a/crates/net/src/pairing/websocket.rs b/crates/net/src/pairing/websocket.rs index fb9e0cf406..95e2c22fcc 100644 --- a/crates/net/src/pairing/websocket.rs +++ b/crates/net/src/pairing/websocket.rs @@ -2,9 +2,8 @@ use super::{DeviceEnrollment, Error, Result, ServerPairUrl}; use crate::NetworkAccount; use futures::{ - select, stream::{SplitSink, SplitStream}, - FutureExt, SinkExt, StreamExt, + SinkExt, StreamExt, }; use prost::bytes::Bytes; use snow::{Builder, HandshakeState, Keypair, TransportState}; @@ -70,7 +69,7 @@ enum IncomingAction { HandleMessage(PairingMessage), } -/// Listen for incoming messages on the stream. +/// Listen for incoming messages on the websocket stream. async fn listen( mut rx: WsStream, tx: mpsc::Sender, @@ -102,6 +101,7 @@ async fn listen( } } } + tracing::debug!("pairing::websocket::connection_closed"); } /// Offer is the device that is authenticated and can @@ -217,26 +217,29 @@ impl<'a> OfferPairing<'a> { let (close_tx, mut close_rx) = mpsc::channel::<()>(1); tokio::task::spawn(listen(stream, offer_tx, close_tx)); loop { - select! { - event = offer_rx.recv().fuse() => { - if let Some(event) = event { - self.incoming(event).await?; - if self.is_finished() { - break; - } + tokio::select! { + biased; + // Explicit shutdown notification + Some(_) = shutdown_rx.recv() => { + tracing::debug!("pairing::offer::shutdown_received"); + if let Err(error) = self.tx.send(Message::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Utf8Bytes::from_static("closed"), + }))).await { + tracing::error!( + error = %error, + "pairing::offer::websocket_close_frame::error"); } + break; } - event = close_rx.recv().fuse() => { - if event.is_some() { - break; - } + // Close signal from the websocket stream + Some(_) = close_rx.recv() => { + break; } - event = shutdown_rx.recv().fuse() => { - if event.is_some() { - let _ = self.tx.send(Message::Close(Some(CloseFrame { - code: CloseCode::Normal, - reason: Utf8Bytes::from_static("closed"), - }))).await; + // Incoming event + Some(event) = offer_rx.recv() => { + self.incoming(event).await?; + if self.is_finished() { break; } } @@ -633,8 +636,18 @@ impl<'a> AcceptPairing<'a> { tokio::task::spawn(listen(stream, offer_tx, close_tx)); loop { - select! { - event = offer_rx.recv().fuse() => { + tokio::select! { + biased; + event = shutdown_rx.recv() => { + if event.is_some() { + let _ = self.tx.send(Message::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Utf8Bytes::from_static("closed"), + }))).await; + break; + } + } + event = offer_rx.recv() => { if let Some(event) = event { self.incoming(event).await?; if self.is_finished() { @@ -642,17 +655,8 @@ impl<'a> AcceptPairing<'a> { } } } - event = close_rx.recv().fuse() => { - if event.is_some() { - break; - } - } - event = shutdown_rx.recv().fuse() => { + event = close_rx.recv() => { if event.is_some() { - let _ = self.tx.send(Message::Close(Some(CloseFrame { - code: CloseCode::Normal, - reason: Utf8Bytes::from_static("closed"), - }))).await; break; } } diff --git a/crates/protocol/src/bindings/mod.rs b/crates/protocol/src/bindings/mod.rs index 8288a365ac..0297988653 100644 --- a/crates/protocol/src/bindings/mod.rs +++ b/crates/protocol/src/bindings/mod.rs @@ -11,7 +11,7 @@ mod sync; pub use diff::{DiffRequest, DiffResponse}; #[cfg(feature = "listen")] -pub use notifications::ChangeNotification; +pub use notifications::NetworkChangeEvent; pub use patch::{PatchRequest, PatchResponse}; #[cfg(feature = "pairing")] #[doc(hidden)] diff --git a/crates/protocol/src/bindings/notifications.rs b/crates/protocol/src/bindings/notifications.rs index bfaa0369bf..c517904cef 100644 --- a/crates/protocol/src/bindings/notifications.rs +++ b/crates/protocol/src/bindings/notifications.rs @@ -6,7 +6,7 @@ use sos_sync::MergeOutcome; /// Notification sent by the server when changes were made. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ChangeNotification { +pub struct NetworkChangeEvent { /// Account identifier. account_id: AccountId, /// Connection identifier that made the change. @@ -17,7 +17,7 @@ pub struct ChangeNotification { outcome: MergeOutcome, } -impl ChangeNotification { +impl NetworkChangeEvent { /// Create a new change notification. pub fn new( account_id: &AccountId, @@ -54,10 +54,10 @@ impl ChangeNotification { } } -impl From +impl From for (AccountId, String, CommitHash, MergeOutcome) { - fn from(value: ChangeNotification) -> Self { + fn from(value: NetworkChangeEvent) -> Self { ( value.account_id, value.connection_id, @@ -67,14 +67,14 @@ impl From } } -impl ProtoBinding for ChangeNotification { - type Inner = WireChangeNotification; +impl ProtoBinding for NetworkChangeEvent { + type Inner = WireNetworkChangeEvent; } -impl TryFrom for ChangeNotification { +impl TryFrom for NetworkChangeEvent { type Error = Error; - fn try_from(value: WireChangeNotification) -> Result { + fn try_from(value: WireNetworkChangeEvent) -> Result { let account_id: [u8; 20] = value.account_id.as_slice().try_into()?; Ok(Self { account_id: account_id.into(), @@ -85,8 +85,8 @@ impl TryFrom for ChangeNotification { } } -impl From for WireChangeNotification { - fn from(value: ChangeNotification) -> WireChangeNotification { +impl From for WireNetworkChangeEvent { + fn from(value: NetworkChangeEvent) -> WireNetworkChangeEvent { Self { account_id: value.account_id().as_ref().to_vec(), connection_id: value.connection_id, diff --git a/crates/protocol/src/network_client/http.rs b/crates/protocol/src/network_client/http.rs index a22b89558e..aa27584e0f 100644 --- a/crates/protocol/src/network_client/http.rs +++ b/crates/protocol/src/network_client/http.rs @@ -34,7 +34,7 @@ use crate::{ network_client::websocket::{ ListenOptions, WebSocketChangeListener, WebSocketHandle, }, - ChangeNotification, + NetworkChangeEvent, }; #[cfg(feature = "files")] @@ -118,7 +118,7 @@ impl HttpClient { pub fn listen( &self, options: ListenOptions, - handler: impl Fn(ChangeNotification) -> F + Send + Sync + 'static, + handler: impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static, ) -> WebSocketHandle where F: Future + Send + 'static, @@ -473,32 +473,51 @@ impl FileSyncClient for HttpClient { let mut bytes_sent = 0; if let Err(error) = progress.send((bytes_sent, Some(file_size))).await { - tracing::warn!(error = ?error); + tracing::warn!( + error = ?error, + "http::progress_send_initial_size", + ); } - let mut reader_stream = ReaderStream::new(file); - - let (tx, rx) = mpsc::channel(8); + let (tx, rx) = mpsc::channel(128); tokio::task::spawn(async move { + let mut reader_stream = ReaderStream::new(file); + let upload_channel = tx.clone(); loop { tokio::select! { biased; - _ = cancel.changed() => { - let reason = cancel.borrow().clone(); - tracing::debug!(reason = ?reason, "upload::canceled"); - if let Err(e) = tx.send(Err(Error::TransferCanceled(reason))).await { - tracing::warn!(error = %e); + _= cancel.changed() => { + let reason = cancel.borrow_and_update().clone(); + if reason != crate::transfer::CancelReason::default() { + tracing::debug!( + reason = ?reason, + "upload::canceled", + ); + if let Err(error) = upload_channel.send(Err(Error::TransferCanceled(reason))).await { + tracing::warn!( + error = %error, + "http::send_transfer_canceled", + ); + } + + break; } } Some(chunk) = reader_stream.next() => { if let Ok(bytes) = &chunk { bytes_sent += bytes.len() as u64; - if let Err(e) = progress.send((bytes_sent, Some(file_size))).await { - tracing::warn!(error = %e); + if let Err(error) = progress.send((bytes_sent, Some(file_size))).await { + tracing::warn!( + error = %error, + "http::send_transfer_progress_update", + ); } } - if let Err(e) = tx.send(chunk.map_err(Error::from)).await { - tracing::error!(error = %e); + if let Err(error) = upload_channel.send(chunk.map_err(Error::from)).await { + tracing::error!( + error = %error, + "http::send_transfer_chunk", + ); break; } } @@ -506,7 +525,7 @@ impl FileSyncClient for HttpClient { } }); - let progress_stream = ReceiverStream::new(rx); + let upload_stream = ReceiverStream::new(rx); // Use a client without the read timeout // as this may be a long running request @@ -522,7 +541,7 @@ impl FileSyncClient for HttpClient { self.request_headers(request, sign_url.as_bytes()).await?; let response = request - .body(Body::wrap_stream(progress_stream)) + .body(Body::wrap_stream(upload_stream)) .send() .await?; let status = response.status(); diff --git a/crates/protocol/src/network_client/websocket.rs b/crates/protocol/src/network_client/websocket.rs index d99227563e..46c5cc05f0 100644 --- a/crates/protocol/src/network_client/websocket.rs +++ b/crates/protocol/src/network_client/websocket.rs @@ -2,7 +2,7 @@ use crate::{ network_client::{NetworkRetry, WebSocketRequest}, transfer::CancelReason, - ChangeNotification, Error, Result, WireEncodeDecode, + Error, NetworkChangeEvent, Result, WireEncodeDecode, }; use futures::{ stream::{Map, SplitStream}, @@ -123,13 +123,13 @@ pub fn changes( impl FnMut( std::result::Result, ) -> Result< - Pin> + Send>>, + Pin> + Send>>, >, > { let (_, read) = stream.split(); read.map( move |message| -> Result< - Pin> + Send>>, + Pin> + Send>>, > { match message { Ok(message) => Ok(Box::pin(async move { @@ -141,11 +141,11 @@ pub fn changes( ) } -async fn decode_notification(message: Message) -> Result { +async fn decode_notification(message: Message) -> Result { match message { Message::Binary(buffer) => { let buf: Bytes = buffer.into(); - let notification = ChangeNotification::decode(buf).await?; + let notification = NetworkChangeEvent::decode(buf).await?; Ok(notification) } _ => Err(Error::NotBinaryWebsocketMessageType), @@ -210,7 +210,7 @@ impl WebSocketChangeListener { /// the handler with the notifications. pub fn spawn( self, - handler: impl Fn(ChangeNotification) -> F + Send + Sync + 'static, + handler: impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static, ) -> WebSocketHandle where F: Future + Send + 'static, @@ -229,7 +229,7 @@ impl WebSocketChangeListener { async fn listen( &self, mut stream: WsStream, - handler: &(impl Fn(ChangeNotification) -> F + Send + Sync + 'static), + handler: &(impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static), ) -> Result<()> where F: Future + Send + 'static, @@ -246,7 +246,10 @@ impl WebSocketChangeListener { code: CloseCode::Normal, reason: Utf8Bytes::from_static("closed"), })).await { - tracing::warn!(error = ?error); + tracing::warn!( + error = ?error, + "ws_client::websocket::close_error", + ); } tracing::debug!("ws_client::shutdown"); return Ok(()); @@ -289,7 +292,7 @@ impl WebSocketChangeListener { async fn connect_loop( &self, - handler: &(impl Fn(ChangeNotification) -> F + Send + Sync + 'static), + handler: &(impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static), ) -> Result<()> where F: Future + Send + 'static, diff --git a/crates/protocol/src/protobuf/notifications.proto b/crates/protocol/src/protobuf/notifications.proto index 1933deac53..49ceacbf7d 100644 --- a/crates/protocol/src/protobuf/notifications.proto +++ b/crates/protocol/src/protobuf/notifications.proto @@ -5,7 +5,7 @@ package notifications; import "protobuf/common.proto"; import "protobuf/sync.proto"; -message WireChangeNotification { +message WireNetworkChangeEvent { // Account identifier. bytes account_id = 1; // Connection identifier. diff --git a/crates/sdk/README.md b/crates/sdk/README.md index 0a534b00ec..e13f659955 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -27,6 +27,7 @@ This crate exports a prelude of common types for low-level access but we encoura | [sos-core](https://docs.rs/sos-core/latest/sos_core/) | Core types and traits; cryptography functions, commit trees and event definitions. | | [sos-database](https://docs.rs/sos-database/latest/sos_database/) | SQLite database backend | | [sos-database-upgrader](https://docs.rs/sos-database-upgrader/latest/sos_database_upgrader/) | Upgrade filesystem backend to database backend | +| [sos-debug-snapshot](https://docs.rs/sos-debug-snapshot/latest/sos_debug_snapshot/) | Create debug snapshot ZIP archives | | [sos-extension-service](https://docs.rs/sos-extension-service/latest/sos_extension_service/) | Browser extension helper service | | [sos-external-files](https://docs.rs/sos-external-files/latest/sos_external_files/) | Helper functions for managing external encrypted file blobs | | [sos-filesystem](https://docs.rs/sos-filesystem/latest/sos_filesystem/) | Legacy filesystem backend | diff --git a/crates/server/src/handlers/account.rs b/crates/server/src/handlers/account.rs index bb8cb95465..5dae50502e 100644 --- a/crates/server/src/handlers/account.rs +++ b/crates/server/src/handlers/account.rs @@ -624,7 +624,7 @@ mod handlers { use std::sync::Arc; #[cfg(feature = "listen")] - use sos_protocol::ChangeNotification; + use sos_protocol::NetworkChangeEvent; #[cfg(feature = "listen")] use crate::handlers::send_notification; @@ -818,7 +818,7 @@ mod handlers { if let Some(conn_id) = caller.connection_id() { let reader = account.read().await; let local_status = reader.sync_status().await?; - let notification = ChangeNotification::new( + let notification = NetworkChangeEvent::new( caller.account_id(), conn_id.to_string(), local_status.root, @@ -868,7 +868,7 @@ mod handlers { #[cfg(feature = "listen")] if outcome.changes > 0 { if let Some(conn_id) = caller.connection_id() { - let notification = ChangeNotification::new( + let notification = NetworkChangeEvent::new( caller.account_id(), conn_id.to_string(), packet.status.root, diff --git a/crates/server/src/handlers/mod.rs b/crates/server/src/handlers/mod.rs index 286442aa0d..08da77cdb9 100644 --- a/crates/server/src/handlers/mod.rs +++ b/crates/server/src/handlers/mod.rs @@ -28,7 +28,7 @@ pub(crate) mod websocket; const BODY_LIMIT: usize = 33554432; #[cfg(feature = "listen")] -use sos_protocol::{ChangeNotification, WireEncodeDecode}; +use sos_protocol::{NetworkChangeEvent, WireEncodeDecode}; use crate::server::{ServerState, State}; @@ -133,7 +133,7 @@ async fn authenticate_endpoint( pub(crate) async fn send_notification( reader: &State, caller: &Caller, - notification: ChangeNotification, + notification: NetworkChangeEvent, ) { // Send notification on the websockets channel match notification.encode().await { diff --git a/crates/sos/Cargo.toml b/crates/sos/Cargo.toml index ab526d184a..ad08122199 100644 --- a/crates/sos/Cargo.toml +++ b/crates/sos/Cargo.toml @@ -21,6 +21,7 @@ sos-cli-helpers.workspace = true sos-client-storage = { workspace = true, features = ["full"] } sos-database = { workspace = true, features = ["full"] } sos-database-upgrader = { workspace = true, features = ["full"] } +sos-debug-snapshot.workspace = true sos-external-files.workspace = true sos-integrity.workspace = true sos-login = { workspace = true, features = ["full"] } diff --git a/crates/sos/src/commands/tools/debug.rs b/crates/sos/src/commands/tools/debug.rs index 4ad292aabf..4200f48e18 100644 --- a/crates/sos/src/commands/tools/debug.rs +++ b/crates/sos/src/commands/tools/debug.rs @@ -3,7 +3,9 @@ use clap::Subcommand; use sos_backend::BackendTarget; use sos_client_storage::ClientStorage; use sos_core::{AccountRef, Paths}; +use sos_debug_snapshot::{export_debug_snapshot, DebugSnapshotOptions}; use sos_sync::SyncStorage; +use std::path::PathBuf; #[derive(Subcommand, Debug)] pub enum Command { @@ -13,6 +15,19 @@ pub enum Command { #[clap(short, long)] account: AccountRef, }, + /// Create a debug snapshot ZIP bundle. + Snapshot { + /// Account name or identifier. + #[clap(short, long)] + account: AccountRef, + + /// Include audit trail. + #[clap(long)] + include_audit_trail: bool, + + /// Output ZIP file. + file: PathBuf, + }, } pub async fn run(cmd: Command) -> Result<()> { @@ -30,6 +45,26 @@ pub async fn run(cmd: Command) -> Result<()> { let debug_tree = storage.debug_account_tree(account_id).await?; serde_json::to_writer_pretty(std::io::stdout(), &debug_tree)?; } + Command::Snapshot { + account, + include_audit_trail, + file, + } => { + let account_id = resolve_account_address(Some(&account)).await?; + let paths = Paths::new_client(Paths::data_dir()?) + .with_account_id(&account_id); + let target = BackendTarget::from_paths(&paths).await?; + + let storage = + ClientStorage::new_unauthenticated(target, &account_id) + .await?; + + let options = DebugSnapshotOptions { + include_audit_trail, + ..Default::default() + }; + export_debug_snapshot(&storage, file, options).await?; + } } Ok(()) diff --git a/crates/sos/src/error.rs b/crates/sos/src/error.rs index 8afeac1121..212e328018 100644 --- a/crates/sos/src/error.rs +++ b/crates/sos/src/error.rs @@ -211,6 +211,10 @@ pub enum Error { #[error(transparent)] Backend(#[from] sos_backend::Error), + /// Error generated by the debug snapshot library. + #[error(transparent)] + DebugSnapshot(#[from] sos_debug_snapshot::Error), + /// Error generated by the backend storage. #[error(transparent)] BackendStorage(#[from] sos_backend::StorageError), diff --git a/crates/storage/client/src/filesystem.rs b/crates/storage/client/src/filesystem.rs index 342a3ae380..8b10089350 100644 --- a/crates/storage/client/src/filesystem.rs +++ b/crates/storage/client/src/filesystem.rs @@ -16,7 +16,7 @@ use sos_core::{ decode, device::TrustedDevice, encode, - events::{DeviceEvent, Event, EventLog, ReadEvent}, + events::{DeviceEvent, Event, EventLog, EventLogType, ReadEvent}, AccountId, Paths, SecretId, VaultFlags, VaultId, }; use sos_filesystem::write_exclusive; @@ -324,8 +324,14 @@ impl ClientFolderStorage for ClientFileSystemStorage { } async fn new_folder(&self, vault: &Vault, _: Internal) -> Result { - let vault_path = self.paths.vault_path(vault.id()); - Ok(Folder::from_path(&vault_path).await?) + let folder_id = *vault.id(); + let vault_path = self.paths.vault_path(&folder_id); + Ok(Folder::from_path( + &vault_path, + self.account_id(), + EventLogType::Folder(folder_id), + ) + .await?) } async fn read_vault(&self, id: &VaultId) -> Result { diff --git a/crates/storage/client/src/traits.rs b/crates/storage/client/src/traits.rs index eab86ca5ff..f797cc1a74 100644 --- a/crates/storage/client/src/traits.rs +++ b/crates/storage/client/src/traits.rs @@ -570,7 +570,8 @@ pub trait ClientFolderStorage: let event_log = folder.event_log(); let mut log_file = event_log.write().await; - compact_folder(folder_id, &mut *log_file).await?; + compact_folder(self.account_id(), folder_id, &mut *log_file) + .await?; } // Refresh in-memory vault and mirrored copy diff --git a/sandbox/.gitignore b/sandbox/.gitignore index 5c532857cf..296754d681 100644 --- a/sandbox/.gitignore +++ b/sandbox/.gitignore @@ -1,10 +1,10 @@ * !.gitignore -!config.toml +!config-filesystem.toml !config-backup.toml !acme.toml !accounts !accounts-backup !acme-cache -!database.toml +!config-database.toml !db_accounts diff --git a/sandbox/accounts/.gitignore b/sandbox/accounts/.gitignore deleted file mode 100644 index d6b7ef32c8..0000000000 --- a/sandbox/accounts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/sandbox/database.toml b/sandbox/config-database.toml similarity index 100% rename from sandbox/database.toml rename to sandbox/config-database.toml diff --git a/sandbox/config.toml b/sandbox/config-filesystem.toml similarity index 100% rename from sandbox/config.toml rename to sandbox/config-filesystem.toml diff --git a/tests/fixtures/backups/v3/0xba0faea9bbc182e3f4fdb3eea7636b5bb31ea9ac.zip b/tests/fixtures/backups/v3/0xba0faea9bbc182e3f4fdb3eea7636b5bb31ea9ac.zip index dad69c1599..d76afdc78b 100644 Binary files a/tests/fixtures/backups/v3/0xba0faea9bbc182e3f4fdb3eea7636b5bb31ea9ac.zip and b/tests/fixtures/backups/v3/0xba0faea9bbc182e3f4fdb3eea7636b5bb31ea9ac.zip differ diff --git a/tests/fixtures/backups/v3/multiple-accounts.zip b/tests/fixtures/backups/v3/multiple-accounts.zip index 9aba661330..d4964a10a2 100644 Binary files a/tests/fixtures/backups/v3/multiple-accounts.zip and b/tests/fixtures/backups/v3/multiple-accounts.zip differ diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 36e512f188..b4f2813192 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -40,9 +40,17 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } http.workspace = true anyhow.workspace = true +[dependencies.sos-changes] +workspace = true +features = [ + "changes-producer", + "changes-consumer", +] + [dependencies.sos-ipc] workspace = true features = [ + "extension-helper-client", "extension-helper-server", "extension-helper-client", "contacts", diff --git a/tests/integration/src/test_extension_helper.rs b/tests/integration/src/test_extension_helper.rs index 1cdb0a143d..f4147a0c50 100644 --- a/tests/integration/src/test_extension_helper.rs +++ b/tests/integration/src/test_extension_helper.rs @@ -1,6 +1,7 @@ use sos_account::{ AccountSwitcherOptions, LocalAccount, LocalAccountSwitcher, }; +use sos_backend::BackendTarget; use sos_ipc::{ extension_helper::server::{ ExtensionHelperOptions, ExtensionHelperServer, @@ -40,6 +41,9 @@ pub async fn main() -> anyhow::Result<()> { ..Default::default() }; let mut accounts = LocalAccountSwitcher::new_with_options(options); + let target = BackendTarget::FileSystem(Paths::new_client( + data_dir.as_ref().unwrap(), + )); accounts .load_accounts( |identity| { @@ -55,7 +59,7 @@ pub async fn main() -> anyhow::Result<()> { .await?) }) }, - data_dir.as_ref(), + target, ) .await?; @@ -66,7 +70,8 @@ pub async fn main() -> anyhow::Result<()> { }; let accounts = Arc::new(RwLock::new(accounts)); let options = ExtensionHelperOptions::new(extension_id, info); - let server = ExtensionHelperServer::new(options, accounts).await?; + let server = + ExtensionHelperServer::new(options, accounts, |_| {}).await?; server.listen().await; Ok(()) } diff --git a/tests/integration/tests/audit_trail/client.rs b/tests/integration/tests/audit_trail/client.rs index 1ab34bced4..5e5b7a02df 100644 --- a/tests/integration/tests/audit_trail/client.rs +++ b/tests/integration/tests/audit_trail/client.rs @@ -1,8 +1,7 @@ use anyhow::Result; use sos_backend::BackendTarget; use sos_client_storage::NewFolderOptions; -use sos_database::open_file; -use sos_test_utils::make_client_backend; +use sos_database::{migrations::migrate_client, open_file}; use crate::test_utils::{mock, setup, teardown}; use futures::{pin_mut, StreamExt}; @@ -11,30 +10,63 @@ use sos_audit::AuditEvent; use sos_migrate::import::ImportTarget; use sos_sdk::prelude::*; use sos_vfs as vfs; -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; #[tokio::test] -async fn audit_trail_client() -> Result<()> { - const TEST_ID: &str = "audit_trail_client"; - // crate::test_utils::init_tracing(); - // +async fn audit_trail_client_fs() -> Result<()> { + const TEST_ID: &str = "audit_trail_client_fs"; + // sos_test_utils::init_tracing(); let mut dirs = setup(TEST_ID, 1).await?; let data_dir = dirs.clients.remove(0); // Configure the audit provider let paths = Paths::new_client(&data_dir); + Paths::scaffold(paths.documents_dir()).await?; paths.ensure().await?; let provider = sos_backend::audit::new_fs_provider(paths.audit_file().to_owned()); sos_backend::audit::init_providers(vec![provider]); - let account_name = TEST_ID.to_string(); + let target = BackendTarget::FileSystem(paths); + run_audit_test(TEST_ID, target).await?; + + teardown(TEST_ID).await; + + Ok(()) +} + +#[tokio::test] +async fn audit_trail_client_db() -> Result<()> { + const TEST_ID: &str = "audit_trail_client_db"; + // sos_test_utils::init_tracing(); + + let mut dirs = setup(TEST_ID, 1).await?; + let data_dir = dirs.clients.remove(0); + + // Configure the audit provider + let paths = Paths::new_client(&data_dir); + let mut client = open_file(paths.database_file()).await?; + migrate_client(&mut client).await?; + + let provider = sos_backend::audit::new_db_provider(client.clone()); + sos_backend::audit::init_providers(vec![provider]); + + let target = BackendTarget::Database(paths, client); + run_audit_test(TEST_ID, target).await?; + + teardown(TEST_ID).await; + + Ok(()) +} + +async fn run_audit_test(name: &str, target: BackendTarget) -> Result<()> { + let account_name = name.to_string(); let (passphrase, _) = generate_passphrase()?; let mut account = LocalAccount::new_account_with_builder( account_name.to_owned(), passphrase.clone(), - make_client_backend(&paths).await?, + target.clone(), |builder| { builder .save_passphrase(false) @@ -51,14 +83,14 @@ async fn audit_trail_client() -> Result<()> { let summary = account.default_folder().await.unwrap(); // Make changes to generate audit logs - simulate_session(&mut account, &summary, &paths).await?; + simulate_session(&mut account, &summary, &target).await?; // Read in the audit log events let events = read_audit_events().await?; let mut kinds: Vec<_> = events.iter().map(|e| e.event_kind()).collect(); //println!("events {:#?}", events); - println!("kinds {:#?}", kinds); + // println!("kinds {:#?}", kinds); // Created the account assert!(matches!(kinds.remove(0), EventKind::CreateAccount)); @@ -121,15 +153,14 @@ async fn audit_trail_client() -> Result<()> { // Imported an account archive assert!(matches!(kinds.remove(0), EventKind::ImportBackupArchive)); - teardown(TEST_ID).await; - Ok(()) } async fn simulate_session( account: &mut LocalAccount, default_folder: &Summary, - paths: &Arc, + target: &BackendTarget, + // paths: &Arc, ) -> Result<()> { // Create a secret let (meta, secret) = mock::note("Audit note", "Note value"); @@ -172,8 +203,10 @@ async fn simulate_session( .rename_folder(new_folder.id(), "New name".to_string()) .await?; - let exported_folder = - paths.documents_dir().join("audit-trail-vault-export.vault"); + let exported_folder = target + .paths() + .documents_dir() + .join("audit-trail-vault-export.vault"); let (export_passphrase, _) = generate_passphrase()?; account .export_folder( @@ -196,13 +229,16 @@ async fn simulate_session( account.delete_folder(new_folder.id()).await?; // Export an account backup archive - let archive = paths + let archive = target + .paths() .documents_dir() .join("audit-trail-exported-archive.zip"); account.export_backup_archive(&archive).await?; - let unsafe_archive = - paths.documents_dir().join("audit-trail-unsafe-archive.zip"); + let unsafe_archive = target + .paths() + .documents_dir() + .join("audit-trail-unsafe-archive.zip"); account.export_unsafe_archive(unsafe_archive).await?; let import_file = "../fixtures/migrate/bitwarden-export.csv"; @@ -217,7 +253,8 @@ async fn simulate_session( let vcard = vfs::read_to_string(contacts).await?; account.import_contacts(&vcard, |_| {}).await?; - let exported_contacts = paths + let exported_contacts = target + .paths() .documents_dir() .join("audit-trail-exported-contacts.vcf"); account.export_all_contacts(exported_contacts).await?; @@ -225,14 +262,7 @@ async fn simulate_session( // Delete the account account.delete_account().await?; - let target = if paths.is_using_db() { - let client = open_file(paths.database_file()).await?; - BackendTarget::Database(paths.clone(), client) - } else { - BackendTarget::FileSystem(paths.clone()) - }; - - LocalAccount::import_backup_archive(archive, &target).await?; + LocalAccount::import_backup_archive(archive, target).await?; Ok(()) } diff --git a/tests/integration/tests/changes/main.rs b/tests/integration/tests/changes/main.rs new file mode 100644 index 0000000000..b6db2d2df1 --- /dev/null +++ b/tests/integration/tests/changes/main.rs @@ -0,0 +1 @@ +mod manual_changes; diff --git a/tests/integration/tests/changes/manual_changes.rs b/tests/integration/tests/changes/manual_changes.rs new file mode 100644 index 0000000000..31296e1575 --- /dev/null +++ b/tests/integration/tests/changes/manual_changes.rs @@ -0,0 +1,71 @@ +use std::time::Duration; + +use anyhow::Result; +use sos_changes::{consumer::ChangeConsumer, producer::ChangeProducer}; +use sos_core::{ + events::{changes_feed, LocalChangeEvent}, + AccountId, Paths, +}; +use sos_test_utils::{setup, teardown}; +use tokio::sync::mpsc; + +/// Dispatch changes via the feed to a producer and consumer. +#[tokio::test] +async fn changes_manual_dispatch() -> Result<()> { + const TEST_ID: &str = "changes_manual_dispatch"; + sos_test_utils::init_tracing(); + + let mut dirs = setup(TEST_ID, 1).await?; + let data_dir = dirs.clients.remove(0); + let paths = Paths::new_client(&data_dir); + + let (tx, mut rx) = mpsc::channel(4); + + // Consumer spawns a task + let mut handle = ChangeConsumer::listen(paths.clone())?; + tokio::task::spawn(async move { + let events = handle.changes(); + while let Some(event) = events.recv().await { + tx.send(event).await.unwrap(); + } + Ok::<_, anyhow::Error>(()) + }); + + // Producer spawns a task + let interval = Duration::from_secs(30); + ChangeProducer::listen(paths.clone(), interval).await?; + + // Simulate producing some events + let send = async move { + let feed = changes_feed(); + feed.send_replace(LocalChangeEvent::AccountCreated( + AccountId::random(), + )); + + // Need to delay between triggering change events + // like in the real world as the changes feed uses + // a watch channel not a broadcast channel + tokio::time::sleep(Duration::from_millis(50)).await; + + feed.send_replace(LocalChangeEvent::AccountDeleted( + AccountId::random(), + )); + }; + + // Listen for the consumer events + let recv = async move { + let mut events = Vec::new(); + while let Some(event) = rx.recv().await { + events.push(event); + if events.len() == 2 { + break; + } + } + }; + + futures::future::join(send, recv).await; + + teardown(TEST_ID).await; + + Ok(()) +} diff --git a/tests/unit/src/tests/account_builder.rs b/tests/unit/src/tests/account_builder.rs index 9cebebccc7..ab19f7ab44 100644 --- a/tests/unit/src/tests/account_builder.rs +++ b/tests/unit/src/tests/account_builder.rs @@ -15,6 +15,7 @@ async fn account_builder_fs() -> Result<()> { let dirs = setup(TEST_ID, 1).await?; let paths = Paths::new_client(&dirs.test_dir); + Paths::scaffold(paths.documents_dir()).await?; let account_name = "fs-account".to_owned(); let password = memorable(); diff --git a/tests/unit/src/tests/client_storage.rs b/tests/unit/src/tests/client_storage.rs index d2f3fca210..a6f2ae6ff5 100644 --- a/tests/unit/src/tests/client_storage.rs +++ b/tests/unit/src/tests/client_storage.rs @@ -26,7 +26,7 @@ use sos_sync::{CreateSet, StorageEventLogs, SyncStorage}; use sos_test_utils::{mock, setup, teardown}; use sos_vault::secret::SecretRow; use std::collections::HashMap; -use tempfile::tempdir_in; +use tempfile::tempdir; const ACCOUNT_NAME: &str = "client_storage"; const MAIN_NAME: &str = "main"; @@ -41,7 +41,9 @@ const MOCK_VALUE_UPDATED: &str = "mock-value-updated"; #[tokio::test] async fn fs_client_storage() -> Result<()> { - let temp = tempdir_in("target")?; + // Windows doesn't like using tempdir_in("target") here + // but it works in other tests! + let temp = tempdir()?; Paths::scaffold(&temp.path().to_owned()).await?; let account_id = AccountId::random(); @@ -62,6 +64,8 @@ async fn fs_client_storage() -> Result<()> { ) .await?; + temp.close()?; + Ok(()) } diff --git a/tests/unit/src/tests/event_log/load_tree.rs b/tests/unit/src/tests/event_log/load_tree.rs index 28f60c1440..adf8a17404 100644 --- a/tests/unit/src/tests/event_log/load_tree.rs +++ b/tests/unit/src/tests/event_log/load_tree.rs @@ -2,7 +2,11 @@ use super::mock; use anyhow::Result; use futures::{pin_mut, StreamExt}; use sos_backend::{BackendEventLog, BackendTarget, FolderEventLog}; -use sos_core::{commit::CommitHash, events::EventLog, Paths}; +use sos_core::{ + commit::CommitHash, + events::{EventLog, EventLogType}, + AccountId, Paths, VaultId, +}; use sos_test_utils::mock::file_database; #[tokio::test] @@ -10,8 +14,14 @@ async fn fs_event_log_load_tree() -> Result<()> { let path = "target/event_log_file_load.events"; let (mock_event_log, _) = mock::fs_event_log_standalone(path).await?; let expected_root = mock_event_log.tree().root().unwrap(); + let account_id = AccountId::random(); let event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path).await?, + sos_filesystem::FolderEventLog::new_folder( + path, + account_id, + EventLogType::Folder(VaultId::new_v4()), + ) + .await?, ); assert_load_tree(event_log, expected_root).await?; Ok(()) diff --git a/tests/unit/src/tests/event_log/mod.rs b/tests/unit/src/tests/event_log/mod.rs index 4596bad63e..fdd2e50a5e 100644 --- a/tests/unit/src/tests/event_log/mod.rs +++ b/tests/unit/src/tests/event_log/mod.rs @@ -15,7 +15,7 @@ pub mod mock { commit::{CommitHash, CommitTree}, crypto::PrivateKey, encode, - events::{EventLog, WriteEvent}, + events::{EventLog, EventLogType, WriteEvent}, AccountId, Paths, SecretId, VaultCommit, VaultEntry, VaultId, }; use sos_database::async_sqlite::Client; @@ -171,9 +171,16 @@ pub mod mock { let (id, data) = mock_secret().await?; + let account_id = AccountId::random(); + // Create a simple event log let mut event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path.as_ref()).await?, + sos_filesystem::FolderEventLog::new_folder( + path.as_ref(), + account_id, + EventLogType::Folder(VaultId::new_v4()), + ) + .await?, ); event_log .apply(&[ @@ -206,9 +213,17 @@ pub mod mock { let (id, data) = mock_secret().await?; + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + // Create a simple event log let mut server = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(server_file).await?, + sos_filesystem::FolderEventLog::new_folder( + server_file, + account_id, + log_type, + ) + .await?, ); server .apply(&[ @@ -219,7 +234,12 @@ pub mod mock { // Duplicate the server events on the client let mut client = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(client_file).await?, + sos_filesystem::FolderEventLog::new_folder( + client_file, + account_id, + log_type, + ) + .await?, ); { let stream = server.event_stream(false).await; diff --git a/tests/unit/src/tests/event_log/rewind.rs b/tests/unit/src/tests/event_log/rewind.rs index 5586daf591..52c4dcdafd 100644 --- a/tests/unit/src/tests/event_log/rewind.rs +++ b/tests/unit/src/tests/event_log/rewind.rs @@ -1,11 +1,11 @@ use super::mock; use anyhow::Result; use sos_backend::{BackendEventLog, BackendTarget, FolderEventLog}; -use sos_core::Paths; use sos_core::{ commit::CommitHash, encode, - events::{EventLog, WriteEvent}, + events::{EventLog, EventLogType, WriteEvent}, + AccountId, Paths, VaultId, }; use sos_test_utils::mock::memory_database; use sos_vault::Vault; @@ -18,15 +18,24 @@ async fn fs_event_log_rewind() -> Result<()> { vfs::remove_file(path).await?; } + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + let vault: Vault = Default::default(); let event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path).await?, + sos_filesystem::FolderEventLog::new_folder( + path, account_id, log_type, + ) + .await?, ); let rewind_root = assert_event_log_rewind(event_log, vault).await?; // Create new event log to load the commits and verify the root let event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path).await?, + sos_filesystem::FolderEventLog::new_folder( + path, account_id, log_type, + ) + .await?, ); assert_event_log_rewound_root(event_log, rewind_root).await?; diff --git a/tests/unit/src/tests/folder.rs b/tests/unit/src/tests/folder.rs index b5d49bf058..df2c5cce4b 100644 --- a/tests/unit/src/tests/folder.rs +++ b/tests/unit/src/tests/folder.rs @@ -2,10 +2,11 @@ use anyhow::Result; use futures::{pin_mut, StreamExt}; use secrecy::ExposeSecret; use sos_backend::{BackendTarget, Folder, FolderEventLog}; -use sos_core::Paths; +use sos_core::events::EventLogType; use sos_core::{ crypto::AccessKey, encode, events::EventLog, SecretId, VaultFlags, }; +use sos_core::{AccountId, Paths, VaultId}; use sos_test_utils::mock::{ self, file_database, insert_database_vault, vault_file, vault_memory, }; @@ -21,7 +22,11 @@ async fn fs_folder_lifecycle() -> Result<()> { let buffer = encode(&vault).await?; vfs::write(temp.path(), &buffer).await?; - let mut folder = Folder::from_path(temp.path()).await?; + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + + let mut folder = + Folder::from_path(temp.path(), &account_id, log_type).await?; let key: AccessKey = password.into(); assert_folder(&mut folder, key).await?; @@ -40,12 +45,13 @@ async fn db_folder_lifecycle() -> Result<()> { let buffer = encode(&vault).await?; vfs::write(temp.path(), &buffer).await?; let paths = Paths::new_client(dir.path()); - let target = BackendTarget::Database(paths, client); + let target = BackendTarget::Database(paths, client.clone()); let mut folder = Folder::new(target, &account_id, vault.id()).await?; let key: AccessKey = password.into(); assert_folder(&mut folder, key).await?; + client.close().await?; temp.close()?; dir.close()?; Ok(()) diff --git a/tests/unit/src/tests/mod.rs b/tests/unit/src/tests/mod.rs index 4fe71f6942..b7469b9ba1 100644 --- a/tests/unit/src/tests/mod.rs +++ b/tests/unit/src/tests/mod.rs @@ -14,7 +14,7 @@ mod identity; mod keychain; mod migrate; mod not_authenticated; -mod pairing_share_url; +mod pairing_url; mod password; mod preferences; mod protocol_encoding; diff --git a/tests/unit/src/tests/pairing_share_url.rs b/tests/unit/src/tests/pairing_share_url.rs deleted file mode 100644 index 879e6c60ed..0000000000 --- a/tests/unit/src/tests/pairing_share_url.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Result; -use sos_core::AccountId; -use sos_net::pairing::ServerPairUrl; -use url::Url; - -#[test] -fn server_pair_url() -> Result<()> { - let mock_account_id = AccountId::random(); - let mock_url = Url::parse("http://192.168.1.8:5053/foo?bar=baz+qux")?; - let mock_key = vec![1, 2, 3, 4]; - let share = ServerPairUrl::new( - mock_account_id.clone(), - mock_url.clone(), - mock_key.clone(), - ); - let share_url: Url = share.into(); - let share_url = share_url.to_string(); - let parsed_share: ServerPairUrl = share_url.parse()?; - assert_eq!(&mock_account_id, parsed_share.account_id()); - assert_eq!(&mock_url, parsed_share.server()); - assert_eq!(&mock_key, parsed_share.public_key()); - Ok(()) -} diff --git a/tests/unit/src/tests/pairing_url.rs b/tests/unit/src/tests/pairing_url.rs new file mode 100644 index 0000000000..3b8f04911e --- /dev/null +++ b/tests/unit/src/tests/pairing_url.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use sos_core::AccountId; +use sos_net::pairing::ServerPairUrl; +use url::Url; + +const SERVER: &str = "http://192.168.1.8:5053"; + +#[test] +fn pair_server_url() -> Result<()> { + let mock_account_id = AccountId::random(); + let mock_url = Url::parse(&format!("{}/foo?bar=baz+qux", SERVER))?; + let mock_key = vec![1, 2, 3, 4]; + let share = ServerPairUrl::new( + mock_account_id.clone(), + mock_url.clone(), + mock_key.clone(), + ); + let share_url: Url = share.into(); + let share_url = share_url.to_string(); + let parsed_share: ServerPairUrl = share_url.parse()?; + assert_eq!(&mock_account_id, parsed_share.account_id()); + assert_eq!(&mock_url, parsed_share.server()); + assert_eq!(&mock_key, parsed_share.public_key()); + Ok(()) +} + +#[test] +fn pair_server_url_errors() -> Result<()> { + // Not data:// scheme + assert!(SERVER.parse::().is_err()); + // Invalid path for MIME type info + assert!("data://image/png,sos-pair" + .parse::() + .is_err()); + // No `aid` query string + assert!("data://text/plain,sos-pair" + .parse::() + .is_err()); + // No server `url` query string + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c" + .parse::() + .is_err()); + // No noise public `key` query string + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c&url=http://localhost" + .parse::() + .is_err()); + // No pre-shared private `psk` query string + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c&url=http://localhost&key=0xff" + .parse::() + .is_err()); + // The `psk` query string is not 32 bytes + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c&url=http://localhost&key=0xff&psk=0xff" + .parse::() + .is_err()); + + Ok(()) +} diff --git a/tests/unit/src/tests/protocol_encoding.rs b/tests/unit/src/tests/protocol_encoding.rs index 87dcbd8895..2c53942ac3 100644 --- a/tests/unit/src/tests/protocol_encoding.rs +++ b/tests/unit/src/tests/protocol_encoding.rs @@ -231,13 +231,13 @@ async fn encode_decode_merge_outcom() -> Result<()> { #[cfg(feature = "listen")] #[tokio::test] async fn encode_decode_change_notification() -> Result<()> { - use sos_protocol::ChangeNotification; + use sos_protocol::NetworkChangeEvent; let outcome = MergeOutcome { changes: 7, ..Default::default() }; let account_id: AccountId = [1u8; 20].into(); - let value = ChangeNotification::new( + let value = NetworkChangeEvent::new( &account_id, "mock-connection".to_string(), Default::default(), @@ -245,7 +245,7 @@ async fn encode_decode_change_notification() -> Result<()> { ); let buffer = value.clone().encode().await?; let buffer: Bytes = buffer.into(); - let decoded = ChangeNotification::decode(buffer).await?; + let decoded = NetworkChangeEvent::decode(buffer).await?; assert_eq!(value, decoded); Ok(()) } diff --git a/tests/unit/src/tests/sdk/reducer.rs b/tests/unit/src/tests/sdk/reducer.rs index e28888fea6..d041cd6296 100644 --- a/tests/unit/src/tests/sdk/reducer.rs +++ b/tests/unit/src/tests/sdk/reducer.rs @@ -2,8 +2,10 @@ use anyhow::Result; use secrecy::ExposeSecret; use sos_backend::{BackendEventLog, FolderEventLog}; use sos_core::{ - crypto::PrivateKey, decode, events::EventLog, SecretId, VaultCommit, - VaultEntry, + crypto::PrivateKey, + decode, + events::{EventLog, EventLogType}, + AccountId, SecretId, VaultCommit, VaultEntry, VaultId, }; use sos_reducers::FolderReducer; use sos_test_utils::mock; @@ -73,10 +75,17 @@ async fn event_log_reduce_compact() -> Result<()> { assert_eq!(2, events.len()); + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + let compact_temp = NamedTempFile::new()?; let mut compact = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(compact_temp.path()) - .await?, + sos_filesystem::FolderEventLog::new_folder( + compact_temp.path(), + account_id, + log_type, + ) + .await?, ); compact.apply(events.as_slice()).await?; @@ -96,9 +105,17 @@ async fn mock_event_log_file( let (encryption_key, _, _) = mock::encryption_key()?; let (_, mut vault, _) = mock::vault_file().await?; + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + let temp = NamedTempFile::new()?; let mut event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(temp.path()).await?, + sos_filesystem::FolderEventLog::new_folder( + temp.path(), + account_id, + log_type, + ) + .await?, ); // Create the vault diff --git a/tests/utils/src/lib.rs b/tests/utils/src/lib.rs index 73d3e007c9..74c1474998 100644 --- a/tests/utils/src/lib.rs +++ b/tests/utils/src/lib.rs @@ -61,6 +61,7 @@ pub async fn make_client_backend( sos_database::migrations::migrate_client(&mut client).await?; BackendTarget::Database(paths.clone(), client) } else { + Paths::scaffold(paths.documents_dir()).await?; BackendTarget::FileSystem(paths.clone()) }) }