From 874f103752befe7c95cd79c026783736f68a8c55 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 17 Sep 2026 06:16:01 +0000 Subject: [PATCH] chore: restructure concord crate (#50) Reviewed-on: https://git.reya.info/reya/coop/pulls/50 --- Cargo.lock | 200 +++++++++++------- .../src/{stream.rs => cords/cord01.rs} | 0 .../src/{ => cords/cord02}/guestbook.rs | 10 +- crates/concord/src/{ => cords/cord02}/list.rs | 12 +- .../src/{control.rs => cords/cord02/mod.rs} | 32 +-- .../concord/src/{chat.rs => cords/cord03.rs} | 10 +- .../src/{edition.rs => cords/cord04/mod.rs} | 5 +- crates/concord/src/{ => cords/cord04}/pins.rs | 20 +- .../concord/src/{ => cords/cord04}/roles.rs | 6 +- .../src/{invite.rs => cords/cord05.rs} | 18 +- .../concord/src/{rekey.rs => cords/cord06.rs} | 62 +++--- crates/concord/src/cords/mod.rs | 9 + crates/concord/src/lib.rs | 152 +------------ crates/concord/src/store.rs | 21 +- crates/concord/src/types/mod.rs | 98 +++++++++ crates/concord/src/{ => utils}/derive.rs | 0 crates/concord/src/utils/mod.rs | 40 ++++ docs/concord-usage.md | 129 +++++------ 18 files changed, 446 insertions(+), 378 deletions(-) rename crates/concord/src/{stream.rs => cords/cord01.rs} (100%) rename crates/concord/src/{ => cords/cord02}/guestbook.rs (99%) rename crates/concord/src/{ => cords/cord02}/list.rs (97%) rename crates/concord/src/{control.rs => cords/cord02/mod.rs} (98%) rename crates/concord/src/{chat.rs => cords/cord03.rs} (99%) rename crates/concord/src/{edition.rs => cords/cord04/mod.rs} (99%) rename crates/concord/src/{ => cords/cord04}/pins.rs (97%) rename crates/concord/src/{ => cords/cord04}/roles.rs (99%) rename crates/concord/src/{invite.rs => cords/cord05.rs} (98%) rename crates/concord/src/{rekey.rs => cords/cord06.rs} (97%) create mode 100644 crates/concord/src/cords/mod.rs create mode 100644 crates/concord/src/types/mod.rs rename crates/concord/src/{ => utils}/derive.rs (100%) create mode 100644 crates/concord/src/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7dce1c28..75038728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.47" +version = "0.4.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef217a77a86a6e3dab9a5b3c81dc445b603fe743a90c1cb10a2f2144628d8cfa" +checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd" dependencies = [ "compression-codecs", "compression-core", @@ -437,7 +437,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -903,7 +903,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1008,9 +1008,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -1244,7 +1244,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "gpui_util", "indexmap", @@ -1286,9 +1286,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257c7085cbb71be72d8fb97edff08b03d86d5d9f2222b9cc34a6bec87093bc10" +checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9" dependencies = [ "bzip2", "compression-core", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "proc-macro2", "quote", @@ -1881,7 +1881,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users 0.5.3", "windows-sys 0.61.2", ] @@ -1911,7 +1911,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2352,7 +2352,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2469,7 +2469,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -2706,7 +2706,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "accesskit", "anyhow", @@ -2735,6 +2735,8 @@ dependencies = [ "log", "lyon", "num_cpus", + "objc2-core-foundation", + "objc2-core-video", "parking", "parking_lot", "pin-project", @@ -2788,7 +2790,7 @@ dependencies = [ [[package]] name = "gpui_apple" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "block", @@ -2812,7 +2814,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "accesskit", "accesskit_unix", @@ -2858,13 +2860,12 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "accesskit", "accesskit_macos", "anyhow", "async-task", - "block", "block2 0.6.2", "cocoa 0.26.0", "collections", @@ -2884,11 +2885,11 @@ dependencies = [ "libc", "log", "mach2", - "media", - "metal", "objc", "objc2 0.6.4", "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-media", "objc2-foundation 0.3.2", "objc2-screen-capture-kit", "objc2-user-notifications", @@ -2905,7 +2906,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2916,7 +2917,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "console_error_panic_hook", "gpui", @@ -2929,7 +2930,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "schemars", "serde", @@ -2939,7 +2940,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "gpui", @@ -2950,7 +2951,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "log", @@ -2960,7 +2961,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "console_error_panic_hook", @@ -2988,7 +2989,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "bytemuck", @@ -3014,7 +3015,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "accesskit", "accesskit_windows", @@ -3289,7 +3290,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "async-compression", @@ -3309,7 +3310,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "log", "rustls", @@ -4085,20 +4086,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "media" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" -dependencies = [ - "anyhow", - "bindgen", - "core-foundation 0.10.1", - "core-video", - "foreign-types", - "metal", - "objc", -] - [[package]] name = "memchr" version = "2.8.3" @@ -4225,7 +4212,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -4677,6 +4664,28 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -4735,6 +4744,34 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -4834,8 +4871,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" dependencies = [ "block2 0.6.2", + "dispatch2", "objc2 0.6.4", "objc2-core-graphics", + "objc2-core-media", "objc2-foundation 0.3.2", ] @@ -5078,7 +5117,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "collections", "serde", @@ -5829,11 +5868,10 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" dependencies = [ - "getrandom 0.2.17", "libredox", "thiserror 2.0.20", ] @@ -5855,13 +5893,13 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "derive_refineable", ] @@ -5943,7 +5981,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "bytes", @@ -6086,9 +6124,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ "bitflags 2.13.2", "errno", @@ -6244,7 +6282,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "async-task", "backtrace", @@ -6280,7 +6318,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -6432,7 +6470,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -6443,7 +6481,7 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -6477,7 +6515,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -6841,7 +6879,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "heapless 0.9.3", "log", @@ -6969,9 +7007,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -7004,7 +7042,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -7159,7 +7197,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -7298,7 +7336,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -7697,9 +7735,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "ab72a15cf68d77cb0987d3684aa8a45c5ef827e8cb49ee2f30bfd7ba2feb519f" [[package]] name = "unicode-linebreak" @@ -7801,9 +7839,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0809a01d1ca5a51ca70db32bb2a19157582a526505ef3c19e3b343a59aa5ad" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" dependencies = [ "base64 0.23.1", "http", @@ -7872,7 +7910,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "perf", "quote", @@ -8053,7 +8091,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "wasm-bindgen-shared", ] @@ -9223,7 +9261,7 @@ checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "synstructure", ] @@ -9295,7 +9333,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "zbus_names", "zvariant", "zvariant_utils", @@ -9483,7 +9521,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "synstructure", ] @@ -9537,7 +9575,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -9549,7 +9587,7 @@ checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "anyhow", "chrono", @@ -9566,7 +9604,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" dependencies = [ "tracing", "tracing-subscriber", @@ -9577,7 +9615,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#739fdbef762f7e514d62e1fa650c1b7cac5bbff4" +source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" [[package]] name = "zune-core" @@ -9628,7 +9666,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", "zvariant_utils", ] @@ -9641,7 +9679,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 3.0.5", + "syn 3.0.6", "winnow 1.0.4", ] diff --git a/crates/concord/src/stream.rs b/crates/concord/src/cords/cord01.rs similarity index 100% rename from crates/concord/src/stream.rs rename to crates/concord/src/cords/cord01.rs diff --git a/crates/concord/src/guestbook.rs b/crates/concord/src/cords/cord02/guestbook.rs similarity index 99% rename from crates/concord/src/guestbook.rs rename to crates/concord/src/cords/cord02/guestbook.rs index 95a7db88..8e044734 100644 --- a/crates/concord/src/guestbook.rs +++ b/crates/concord/src/cords/cord02/guestbook.rs @@ -6,13 +6,13 @@ use anyhow::Result; use data_encoding::HEXLOWER; use nostr_sdk::prelude::*; -use crate::edition::{ - AuthorityCitation, TAG_CITATION, canonical_decimal, citation_from, citation_tag, -}; -use crate::stream::{ +use crate::cord01::{ KIND_WRAP, OpenedStream, SealForm, StreamError, build_rumor_ms, build_seal, open_wrap, wrap_seal, }; +use crate::cord04::{ + AuthorityCitation, TAG_CITATION, canonical_decimal, citation_from, citation_tag, +}; use crate::{GroupKey, decode_hex_32}; pub const KIND_JOIN_LEAVE: u16 = 3306; @@ -532,8 +532,8 @@ fn pubkey(hex: &str, name: &'static str) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::cord01::build_rumor_secs; use crate::derive::guestbook_group_key; - use crate::stream::build_rumor_secs; use crate::{CommunityId, Epoch}; const ROOT: [u8; 32] = [0x5au8; 32]; diff --git a/crates/concord/src/list.rs b/crates/concord/src/cords/cord02/list.rs similarity index 97% rename from crates/concord/src/list.rs rename to crates/concord/src/cords/cord02/list.rs index 0979359c..cd807983 100644 --- a/crates/concord/src/list.rs +++ b/crates/concord/src/cords/cord02/list.rs @@ -5,8 +5,8 @@ use std::fmt; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; -use crate::invite::{ChannelGrant, CommunityInvite}; -use crate::stream::{self, NIP44_MAX_PLAINTEXT}; +use crate::cord01::{self, NIP44_MAX_PLAINTEXT}; +use crate::cord05::{ChannelGrant, CommunityInvite}; use crate::{CommunityId, Epoch, Extra}; pub const KIND_COMMUNITY_LIST: u16 = 13302; @@ -42,8 +42,8 @@ impl fmt::Display for ListError { impl std::error::Error for ListError {} -impl From for ListError { - fn from(error: stream::StreamError) -> Self { +impl From for ListError { + fn from(error: cord01::StreamError) -> Self { ListError::Crypto(error.to_string()) } } @@ -187,7 +187,7 @@ pub fn build_list_event(keys: &Keys, list: &CommunityList) -> Result Result Result Option { let seal = &entry.seal; - if seal.kind.as_u16() != stream::KIND_SEAL_ENCRYPTED || seal.verify().is_err() { + if seal.kind.as_u16() != cord01::KIND_SEAL_ENCRYPTED || seal.verify().is_err() { return None; } @@ -376,7 +376,7 @@ fn verify_edit_bundle( let seal = &bundle.seal; // Checkable before any crypto: nobody else may revise another member's words. - if seal.kind.as_u16() != stream::KIND_SEAL_ENCRYPTED || seal.pubkey != *original_author { + if seal.kind.as_u16() != cord01::KIND_SEAL_ENCRYPTED || seal.pubkey != *original_author { return None; } @@ -456,7 +456,7 @@ fn serialize_sealed( } let inner = encode_form(entries)?; - let sealed = stream::seal_bytes(group.conversation(), inner.as_bytes()) + let sealed = cord01::seal_bytes(group.conversation(), inner.as_bytes()) .map_err(|error| PinError::Seal(error.to_string()))?; let content = serde_json::json!({ "epoch": epoch.to_string(), "sealed": sealed }).to_string(); @@ -526,7 +526,7 @@ pub fn read_list(content: &str, unseal: impl Fn(Epoch) -> Option) -> R }; }; - let Ok(inner) = stream::open_bytes(group.conversation(), sealed) else { + let Ok(inner) = cord01::open_bytes(group.conversation(), sealed) else { return EMPTY; }; @@ -554,7 +554,7 @@ mod tests { use nostr::nips::nip44::v2::{self, ConversationKey}; use super::*; - use crate::chat::{ChatRumor, build_delete, build_edit, build_message, open, seal_rumor}; + use crate::cord03::{ChatRumor, build_delete, build_edit, build_message, open, seal_rumor}; use crate::derive::channel_group_key; const AT_MS: u64 = 1_700_000_000_000; @@ -657,7 +657,7 @@ mod tests { assert!(verify_entry(&forged, &channel()).is_none()); // A rumor carrying a claimed id that is not its own is refused. - let plaintext = stream::open_bytes(&conversation(), &opened.seal.content).expect("opens"); + let plaintext = cord01::open_bytes(&conversation(), &opened.seal.content).expect("opens"); let mut value: serde_json::Value = serde_json::from_slice(&plaintext).expect("json"); value["id"] = serde_json::Value::String("00".repeat(32)); @@ -668,7 +668,7 @@ mod tests { ) .expect("encrypts"); let content = BASE64.encode(&raw); - let seal = EventBuilder::new(Kind::Custom(stream::KIND_SEAL_ENCRYPTED), &content) + let seal = EventBuilder::new(Kind::Custom(cord01::KIND_SEAL_ENCRYPTED), &content) .custom_created_at(opened.seal.created_at) .finalize(&author) .expect("signs"); diff --git a/crates/concord/src/roles.rs b/crates/concord/src/cords/cord04/roles.rs similarity index 99% rename from crates/concord/src/roles.rs rename to crates/concord/src/cords/cord04/roles.rs index e9d028b9..4fa37e8b 100644 --- a/crates/concord/src/roles.rs +++ b/crates/concord/src/cords/cord04/roles.rs @@ -4,10 +4,10 @@ use anyhow::{Result, bail}; use nostr_sdk::prelude::PublicKey; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::derive::{banlist_locator, grant_locator}; -use crate::edition::{ +use crate::cord04::{ AuthorityCitation, EditionMeta, EntityHead, Floors, ParsedEdition, fold_head, vsk, }; +use crate::derive::{banlist_locator, grant_locator}; use crate::{ChannelId, CommunityId, Extra, RoleId, decode_hex_32}; pub const MAX_ROLES_PER_COMMUNITY: usize = 100; @@ -759,7 +759,7 @@ mod tests { use nostr_sdk::prelude::Keys; use super::*; - use crate::edition::{EditionFields, build_edition, parse_edition}; + use crate::cord04::{EditionFields, build_edition, parse_edition}; const COMMUNITY: [u8; 32] = [0xc0; 32]; const AT: u64 = 1_700_000_000; diff --git a/crates/concord/src/invite.rs b/crates/concord/src/cords/cord05.rs similarity index 98% rename from crates/concord/src/invite.rs rename to crates/concord/src/cords/cord05.rs index 5191b8f7..4550fd94 100644 --- a/crates/concord/src/invite.rs +++ b/crates/concord/src/cords/cord05.rs @@ -10,11 +10,11 @@ use nostr::nips::nip59::{GiftWrapBuilder, UnwrappedGift}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; -use crate::control::{ImageRef, MAX_RELAYS}; +use crate::cord01::{self, NIP44_MAX_PLAINTEXT, StreamError}; +use crate::cord02::list::{canonical, union}; +use crate::cord02::{ImageRef, MAX_RELAYS}; +use crate::cord04::{TAG_SUBKIND, vsk}; use crate::derive::{TOKEN_LEN, verify_community_id}; -use crate::edition::{TAG_SUBKIND, vsk}; -use crate::list::{canonical, union}; -use crate::stream::{self, NIP44_MAX_PLAINTEXT, StreamError}; use crate::{ChannelId, CommunityId, Epoch, Extra, decode_hex_32}; pub const KIND_BUNDLE: u16 = 33301; @@ -594,7 +594,7 @@ pub fn build_invite_list(keys: &Keys, list: &InviteList) -> Result Result InviteEntry { } fn seal_bundle(bundle_key: &[u8; 32], json: &str) -> Result { - Ok(stream::seal_bytes( + Ok(cord01::seal_bytes( &ConversationKey::new(*bundle_key), json.as_bytes(), )?) } fn open_bundle(bundle_key: &[u8; 32], content: &str) -> Result { - let plaintext = stream::open_bytes(&ConversationKey::new(*bundle_key), content)?; + let plaintext = cord01::open_bytes(&ConversationKey::new(*bundle_key), content)?; String::from_utf8(plaintext).map_err(|_| InviteError::BadFragment("bundle is not utf8")) } @@ -941,7 +941,7 @@ mod tests { assert!(unwrap_direct_invite(&wrap, &stranger).is_err()); // ...and a wrap that opens to some other kind is not an invite. - let rumor = EventBuilder::new(Kind::Custom(crate::chat::KIND_MESSAGE), "hello") + let rumor = EventBuilder::new(Kind::Custom(crate::cord03::KIND_MESSAGE), "hello") .finalize_unsigned(recipient.public_key()); let wrap = GiftWrapBuilder::new(recipient.public_key(), rumor) .finalize(&recipient) diff --git a/crates/concord/src/rekey.rs b/crates/concord/src/cords/cord06.rs similarity index 97% rename from crates/concord/src/rekey.rs rename to crates/concord/src/cords/cord06.rs index 8709f63d..ce124970 100644 --- a/crates/concord/src/rekey.rs +++ b/crates/concord/src/cords/cord06.rs @@ -7,17 +7,17 @@ use nostr::nips::nip44::v2::ConversationKey; use nostr_sdk::prelude::{Event, Keys, PublicKey, SecretKey, Tag, Timestamp, UnsignedEvent}; use serde::{Deserialize, Serialize}; -use crate::control::CommunityIdentity; +use crate::cord01::{self, KIND_SEAL_PLAINTEXT, OpenedStream, SealForm, StreamError}; +use crate::cord02::CommunityIdentity; +use crate::cord04::roles::CommunityRoles; +use crate::cord04::{ + AuthorityCitation, KIND_CONTROL, TAG_SUBKIND, canonical_decimal, citation_from, citation_tag, + vsk, +}; use crate::derive::{ base_rekey_group_key, channel_rekey_group_key, control_group_key, control_signer_group_key, dissolved_group_key, epoch_key_commitment, recipient_locator, }; -use crate::edition::{ - AuthorityCitation, KIND_CONTROL, TAG_SUBKIND, canonical_decimal, citation_from, citation_tag, - vsk, -}; -use crate::roles::CommunityRoles; -use crate::stream::{self, KIND_SEAL_PLAINTEXT, OpenedStream, SealForm, StreamError}; use crate::{ChannelId, CommunityId, Epoch, GroupKey, random_32}; pub const KIND_REKEY: u16 = 3303; @@ -325,7 +325,7 @@ pub fn open_blob( ) -> Result { let conversation = ConversationKey::derive(recipient.secret_key(), rotator).map_err(crypto_error)?; - let plaintext = stream::open_bytes(&conversation, &blob.wrapped)?; + let plaintext = cord01::open_bytes(&conversation, &blob.wrapped)?; parse_blob_plaintext(&plaintext, scope, epoch, community_id) } @@ -347,7 +347,7 @@ fn seal_to( plaintext: &[u8], ) -> Result { let conversation = ConversationKey::derive(secret, recipient).map_err(crypto_error)?; - Ok(stream::seal_bytes(&conversation, plaintext)?) + Ok(cord01::seal_bytes(&conversation, plaintext)?) } #[derive(Debug, Clone)] @@ -539,7 +539,7 @@ pub fn compact( let mut wraps = Vec::with_capacity(seals.len()); for seal in seals { - wraps.push(stream::rewrap_seal(seal, read, signer, at)?.0); + wraps.push(cord01::rewrap_seal(seal, read, signer, at)?.0); } Ok(wraps) @@ -592,7 +592,7 @@ pub fn build_rekey_rumor( tags.push(Tag::custom(TAG_SEVER, ["1"])); } - Ok(stream::build_rumor_secs( + Ok(cord01::build_rumor_secs( KIND_REKEY, rotator, &content, tags, at_secs, )) } @@ -633,11 +633,11 @@ pub fn build_rekey_chunks( at_secs, )?; - let seal = stream::build_seal(&rumor, SealForm::Encrypted, group, rotator)?; - let (wrap, _) = stream::wrap_seal( + let seal = cord01::build_seal(&rumor, SealForm::Encrypted, group, rotator)?; + let (wrap, _) = cord01::wrap_seal( &seal, group, - stream::KIND_WRAP, + cord01::KIND_WRAP, Timestamp::from_secs(at_secs), &[], )?; @@ -704,7 +704,7 @@ pub fn parse_rekey_chunk(opened: &OpenedStream) -> Result UnsignedEvent { - stream::build_rumor_secs( + cord01::build_rumor_secs( KIND_CONTROL, owner, "", @@ -738,11 +738,11 @@ pub fn seal_dissolved( at_secs: u64, ) -> Result { let group = dissolved_group_key(community_id).map_err(crypto_error)?; - let seal = stream::build_seal(rumor, SealForm::Plaintext, &group, owner)?; - let (wrap, _) = stream::wrap_seal( + let seal = cord01::build_seal(rumor, SealForm::Plaintext, &group, owner)?; + let (wrap, _) = cord01::wrap_seal( &seal, &group, - stream::KIND_WRAP, + cord01::KIND_WRAP, Timestamp::from_secs(at_secs), &[], )?; @@ -756,7 +756,7 @@ pub fn open_dissolved( community_id: &CommunityId, ) -> Result { let group = dissolved_group_key(community_id).map_err(crypto_error)?; - let opened = stream::open_wrap(wrap, &group)?; + let opened = cord01::open_wrap(wrap, &group)?; if !is_tombstone(&opened.rumor, community_id) { return Err(RekeyError::NotADissolution); @@ -859,13 +859,13 @@ mod tests { use std::collections::BTreeSet; use super::*; - use crate::control::{ + use crate::cord01::KIND_WRAP; + use crate::cord02::{ CommunityMetadata, ControlWriter, Edition, ROOT_EPOCH, fold_control, genesis, open_edition, }; + use crate::cord04::roles::{Grant, Permissions, Role, RoleScope}; + use crate::cord04::{EditionFields, Floors, build_edition}; use crate::derive::{community_id_of, grant_locator}; - use crate::edition::{EditionFields, Floors, build_edition}; - use crate::roles::{Grant, Permissions, Role, RoleScope}; - use crate::stream::KIND_WRAP; use crate::{Extra, RoleId}; const AT: u64 = 1_700_000_000; @@ -1074,7 +1074,7 @@ mod tests { .expect("builds"); assert_eq!(chunks.len(), 1); - let opened = stream::open_wrap(&chunks[0], &group).expect("opens"); + let opened = cord01::open_wrap(&chunks[0], &group).expect("opens"); let chunk = parse_rekey_chunk(&opened).expect("parses"); assert_eq!( chunk.rotator, @@ -1486,7 +1486,7 @@ mod tests { at_secs: AT, }); let seal = - stream::build_seal(&rumor, SealForm::Plaintext, &prior_read, &owner).expect("seals"); + cord01::build_seal(&rumor, SealForm::Plaintext, &prior_read, &owner).expect("seals"); let refounding = plan_refounding(Epoch(1)).expect("plans"); let read = refounding.read(&community_id).expect("derives"); @@ -1497,7 +1497,7 @@ mod tests { compact(std::slice::from_ref(&seal), &read, &signer, AT + 1).expect("compacts"); assert_eq!(compacted.len(), 1); - let reopened = stream::open_wrap_at(&compacted[0], &signer.pk(), read.conversation(), true) + let reopened = cord01::open_wrap_at(&compacted[0], &signer.pk(), read.conversation(), true) .expect("opens"); assert_eq!( reopened.seal.sig, seal.sig, @@ -1508,7 +1508,7 @@ mod tests { // Only a plaintext seal can be carried forward. let encrypted = - stream::build_seal(&rumor, SealForm::Encrypted, &prior_read, &owner).expect("seals"); + cord01::build_seal(&rumor, SealForm::Encrypted, &prior_read, &owner).expect("seals"); assert!(matches!( compact(&[encrypted], &read, &signer, AT + 1), Err(RekeyError::Stream(StreamError::NotRewrappable)) @@ -1550,7 +1550,7 @@ mod tests { // The spec's all-zero `eid` is refused: it would let one owner's genuine // tombstone be re-wrapped at another of their communities and kill it. let zeroed = seal_dissolved( - &stream::build_rumor_secs( + &cord01::build_rumor_secs( KIND_CONTROL, owner.public_key(), "", @@ -1580,10 +1580,10 @@ mod tests { owner: owner.public_key(), owner_salt: other_salt, }; - let seal = stream::open_wrap(&wrap, &dissolved_group_key(&community_id).expect("derives")) + let seal = cord01::open_wrap(&wrap, &dissolved_group_key(&community_id).expect("derives")) .expect("opens") .seal; - let replayed = stream::wrap_seal( + let replayed = cord01::wrap_seal( &seal, &dissolved_group_key(&other_id).expect("derives"), KIND_WRAP, diff --git a/crates/concord/src/cords/mod.rs b/crates/concord/src/cords/mod.rs new file mode 100644 index 00000000..202ccb31 --- /dev/null +++ b/crates/concord/src/cords/mod.rs @@ -0,0 +1,9 @@ +//! One module per CORD document. CORD-07 (audio/video) is unimplemented, and +//! CORD-08's timer rides the Chat and Control planes it edits rather than owning a file. + +pub mod cord01; +pub mod cord02; +pub mod cord03; +pub mod cord04; +pub mod cord05; +pub mod cord06; diff --git a/crates/concord/src/lib.rs b/crates/concord/src/lib.rs index 1114e8d8..880f53e6 100644 --- a/crates/concord/src/lib.rs +++ b/crates/concord/src/lib.rs @@ -1,146 +1,12 @@ -pub mod chat; -pub mod control; -pub mod derive; -pub mod edition; -pub mod guestbook; -pub mod invite; -pub mod list; -pub mod pins; -pub mod rekey; -pub mod roles; +mod cords; +mod types; +mod utils; + pub mod store; -pub mod stream; -use std::fmt; -use std::str::FromStr; +pub use cords::{cord01, cord02, cord03, cord04, cord05, cord06}; +pub use types::{ChannelId, CommunityId, Epoch, RoleId}; +pub use utils::derive::{self, GroupKey}; -use anyhow::{Result, anyhow, bail}; -use data_encoding::HEXLOWER; -pub use derive::GroupKey; -use rand::TryRng as _; -use rand::rngs::SysRng; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -/// Unknown fields a content struct does not model, so a republish cannot wipe them. -pub(crate) type Extra = serde_json::Map; - -macro_rules! hex_id { - ($(#[$meta:meta])* $name:ident) => { - $(#[$meta])* - #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct $name([u8; 32]); - - impl $name { - pub fn from_bytes(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } - - pub fn to_hex(&self) -> String { - HEXLOWER.encode(&self.0) - } - } - - impl From<[u8; 32]> for $name { - fn from(bytes: [u8; 32]) -> Self { - Self(bytes) - } - } - - impl fmt::Display for $name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.to_hex()) - } - } - - impl fmt::Debug for $name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}({})", stringify!($name), self.to_hex()) - } - } - - impl FromStr for $name { - type Err = anyhow::Error; - - fn from_str(value: &str) -> Result { - Ok(Self(decode_hex_32(value)?)) - } - } - - impl Serialize for $name { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&self.to_hex()) - } - } - - impl<'de> Deserialize<'de> for $name { - fn deserialize>(deserializer: D) -> Result { - let value = String::deserialize(deserializer)?; - value.parse().map_err(serde::de::Error::custom) - } - } - }; -} - -hex_id! { - /// A self-certifying commitment to the owner's key, never on the wire. - CommunityId -} - -hex_id! { - ChannelId -} - -hex_id! { - /// Both a Role's entity coordinate and the field it repeats in its own content. - RoleId -} - -/// A key-rotation counter; it bumps only on a Rekey that removes somebody. -#[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize, -)] -pub struct Epoch(pub u64); - -impl fmt::Display for Epoch { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Uppercase and other non-canonical spellings are rejected. -pub(crate) fn decode_hex_32(value: &str) -> Result<[u8; 32]> { - decode_hex_lower::<32>(value) -} - -pub(crate) fn decode_hex_lower(value: &str) -> Result<[u8; N]> { - let bytes = HEXLOWER - .decode(value.as_bytes()) - .map_err(|error| anyhow!("invalid hex: {error}"))?; - - let decoded: [u8; N] = bytes - .as_slice() - .try_into() - .map_err(|_| anyhow!("expected {N} bytes, got {}", bytes.len()))?; - - if HEXLOWER.encode(&decoded) != value { - bail!("hex must be lowercase and canonical"); - } - - Ok(decoded) -} - -pub(crate) fn fill_random(bytes: &mut [u8]) -> Result<()> { - SysRng - .try_fill_bytes(bytes) - .map_err(|error| anyhow!("os rng: {error}")) -} - -pub(crate) fn random_32() -> Result<[u8; 32]> { - let mut bytes = [0u8; 32]; - fill_random(&mut bytes)?; - Ok(bytes) -} +pub(crate) use types::Extra; +pub(crate) use utils::{decode_hex_32, decode_hex_lower, fill_random, random_32}; diff --git a/crates/concord/src/store.rs b/crates/concord/src/store.rs index 55052065..6092e789 100644 --- a/crates/concord/src/store.rs +++ b/crates/concord/src/store.rs @@ -6,13 +6,13 @@ use anyhow::{Result, anyhow}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; -use crate::chat::{self, ChatRumor, plane_keys}; -use crate::control::{ +use crate::cord01::{KIND_WRAP_EPHEMERAL, OpenedStream}; +use crate::cord02::{ ChannelMetadata, CommunityGenesis, CommunityMetadata, ControlFold, ROOT_EPOCH, }; +use crate::cord03::{self, ChatRumor, plane_keys}; +use crate::cord04::{EntityHead, Floors, ParsedEdition, vsk}; use crate::derive::control_signer_group_key; -use crate::edition::{EntityHead, Floors, ParsedEdition, vsk}; -use crate::stream::{KIND_WRAP_EPHEMERAL, OpenedStream}; use crate::{ChannelId, CommunityId, Epoch, GroupKey}; static LOCAL_KEYS: LazyLock = LazyLock::new(Keys::generate); @@ -31,7 +31,8 @@ pub async fn cache_rumor( channel: &ChannelId, opened: &OpenedStream, ) -> Result { - if chat::expiration_of(&opened.rumor)?.is_some_and(|expiration| expiration <= Timestamp::now()) + if cord03::expiration_of(&opened.rumor)? + .is_some_and(|expiration| expiration <= Timestamp::now()) { return Ok(false); } @@ -73,7 +74,7 @@ pub async fn purge_expired( continue; }; - let Ok(Some(expiration)) = chat::expiration_of(&rumor) else { + let Ok(Some(expiration)) = cord03::expiration_of(&rumor) else { continue; }; @@ -374,7 +375,7 @@ fn advance( continue; }; - let Ok((opened, rumor)) = chat::open(wrap, group, channel, *epoch) else { + let Ok((opened, rumor)) = cord03::open(wrap, group, channel, *epoch) else { continue; }; @@ -419,11 +420,11 @@ mod tests { use super::*; use crate::Epoch; - use crate::chat::{build_message, seal_rumor}; - use crate::derive::channel_group_key; - use crate::stream::{ + use crate::cord01::{ KIND_WRAP, SealForm, build_rumor_ms, build_seal, channel_binding_tags, open_wrap, wrap_seal, }; + use crate::cord03::{build_message, seal_rumor}; + use crate::derive::channel_group_key; const SECRET: [u8; 32] = [0x07u8; 32]; const NEXT_SECRET: [u8; 32] = [0x11u8; 32]; diff --git a/crates/concord/src/types/mod.rs b/crates/concord/src/types/mod.rs new file mode 100644 index 00000000..0b9741e3 --- /dev/null +++ b/crates/concord/src/types/mod.rs @@ -0,0 +1,98 @@ +use std::fmt; +use std::str::FromStr; + +use anyhow::Result; +use data_encoding::HEXLOWER; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::decode_hex_32; + +/// Unknown fields a content struct does not model, so a republish cannot wipe them. +pub(crate) type Extra = serde_json::Map; + +macro_rules! hex_id { + ($(#[$meta:meta])* $name:ident) => { + $(#[$meta])* + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name([u8; 32]); + + impl $name { + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn to_hex(&self) -> String { + HEXLOWER.encode(&self.0) + } + } + + impl From<[u8; 32]> for $name { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_hex()) + } + } + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}({})", stringify!($name), self.to_hex()) + } + } + + impl FromStr for $name { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + Ok(Self(decode_hex_32(value)?)) + } + } + + impl Serialize for $name { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_hex()) + } + } + + impl<'de> Deserialize<'de> for $name { + fn deserialize>(deserializer: D) -> Result { + let value = String::deserialize(deserializer)?; + value.parse().map_err(serde::de::Error::custom) + } + } + }; +} + +hex_id! { + /// A self-certifying commitment to the owner's key, never on the wire. + CommunityId +} + +hex_id! { + ChannelId +} + +hex_id! { + /// Both a Role's entity coordinate and the field it repeats in its own content. + RoleId +} + +/// A key-rotation counter; it bumps only on a Rekey that removes somebody. +#[derive( + Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize, +)] +pub struct Epoch(pub u64); + +impl fmt::Display for Epoch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/concord/src/derive.rs b/crates/concord/src/utils/derive.rs similarity index 100% rename from crates/concord/src/derive.rs rename to crates/concord/src/utils/derive.rs diff --git a/crates/concord/src/utils/mod.rs b/crates/concord/src/utils/mod.rs new file mode 100644 index 00000000..622d1942 --- /dev/null +++ b/crates/concord/src/utils/mod.rs @@ -0,0 +1,40 @@ +pub mod derive; + +use anyhow::{Result, anyhow, bail}; +use data_encoding::HEXLOWER; +use rand::TryRng as _; +use rand::rngs::SysRng; + +/// Uppercase and other non-canonical spellings are rejected. +pub(crate) fn decode_hex_32(value: &str) -> Result<[u8; 32]> { + decode_hex_lower::<32>(value) +} + +pub(crate) fn decode_hex_lower(value: &str) -> Result<[u8; N]> { + let bytes = HEXLOWER + .decode(value.as_bytes()) + .map_err(|error| anyhow!("invalid hex: {error}"))?; + + let decoded: [u8; N] = bytes + .as_slice() + .try_into() + .map_err(|_| anyhow!("expected {N} bytes, got {}", bytes.len()))?; + + if HEXLOWER.encode(&decoded) != value { + bail!("hex must be lowercase and canonical"); + } + + Ok(decoded) +} + +pub(crate) fn fill_random(bytes: &mut [u8]) -> Result<()> { + SysRng + .try_fill_bytes(bytes) + .map_err(|error| anyhow!("os rng: {error}")) +} + +pub(crate) fn random_32() -> Result<[u8; 32]> { + let mut bytes = [0u8; 32]; + fill_random(&mut bytes)?; + Ok(bytes) +} diff --git a/docs/concord-usage.md b/docs/concord-usage.md index eb68470e..acddea07 100644 --- a/docs/concord-usage.md +++ b/docs/concord-usage.md @@ -13,32 +13,41 @@ folded independently by every client. ## Modules +Files follow the CORD documents. The frozen derivations of Appendix A and the id +vocabulary are shared substrate — every document calls them — so they live outside +`cords` in `utils::derive` (re-exported as `derive`) and `types`. + | Module | Owns | | --- | --- | +| `cord01` | Private Streams: the seal/wrap envelope and the NIP-44 helpers | +| `cord02` | Communities: identity, epochs, metadata, the Control Plane fold and writer | +| `cord02::guestbook` | Joins, leaves, kicks, snapshots, the member list | +| `cord02::list` | The Community List (a member's own memberships, across devices) | +| `cord03` | Channels: Channel metadata and the Chat Plane | +| `cord04` | Roles: chained editions, parse/hash/fold, the roster, permissions, the banlist | +| `cord04::pins` | Pin Lists, and the key disclosure a keyless reader verifies | +| `cord05` | Invite bundles, links, the Direct Invite, the Invite List | +| `cord06` | Key rotations, refounding, compaction, dissolution | | `derive` | Every frozen HKDF derivation and coordinate | -| `stream` | The CORD-01 envelope: seal, wrap, open, and the NIP-44 helpers | -| `edition` | Chained, versioned editions: parse, hash, fold, floors | -| `roles` | Permissions, roles, grants, the banlist, the authority fixpoint | -| `control` | The Control Plane: genesis, the fold, the writer, metadata | -| `chat` | The Chat Plane: message/reaction/edit/delete builders and the fold | -| `guestbook` | Joins, leaves, kicks, snapshots, the member list | -| `invite` | Invite bundles, links, the Direct Invite, the Invite List | -| `list` | The Community List (a member's own memberships, across devices) | -| `rekey` | Key rotations, refounding, compaction, dissolution | -| `pins` | Pin Lists, and the key disclosure a keyless reader verifies | | `store` | Local rumor cache, the community state document, relay paging | +`CommunityId`, `ChannelId`, `RoleId`, `Epoch` and `Extra` (crate-internal) come from +the private `types` module and are re-exported at the crate root. + +CORD-07 (audio/video) is unimplemented. CORD-08's timer has no file of its own: it +lives in the metadata it reads (`cord02`) and the fold it filters (`cord03`). + Read `CommunityId` as "this community", `ChannelId` as "this channel", `Epoch` as "which key generation". Nothing else in the API needs internal state. ## Creating a community ```rust -use concord::control::{self, CommunityMetadata}; +use concord::cord02::{self, CommunityMetadata}; use concord::store::{self, CommunityState, save_state}; let metadata = CommunityMetadata { name: "Room".into(), ..Default::default() }; -let minted = control::genesis(&owner_keys, &metadata, now_secs)?; +let minted = cord02::genesis(&owner_keys, &metadata, now_secs)?; // minted.identity — community_id, owner, owner_salt (verify() recomputes it) // minted.wraps — the two owner-signed genesis editions, already sealed @@ -53,14 +62,14 @@ join: ```rust use concord::derive::{control_group_key, control_signer_group_key}; -use concord::edition::ParsedEdition; +use concord::cord04::ParsedEdition; let read = control_group_key(&minted.community_root, &minted.identity.community_id, Epoch(0))?; let signer = control_signer_group_key(&minted.control_root, &minted.identity.community_id, Epoch(0))?; let editions: Vec = minted .wraps .iter() - .map(|wrap| control::open_edition(wrap, &read, &signer.pk(), true)) + .map(|wrap| cord02::open_edition(wrap, &read, &signer.pk(), true)) .collect::>()?; let mut state = CommunityState::from_genesis(&minted, &editions, added_at_ms)?; @@ -75,12 +84,12 @@ client explicitly — coop's client is a gossip client with no background refres An invite link resolves to a bundle: ```rust -use concord::invite::{self, BundleState, invite_bundle_key}; +use concord::cord05::{self, BundleState, invite_bundle_key}; -let link = invite::parse_link(url)?; // link_signer, token, bootstrap_relays, naddr +let link = cord05::parse_link(url)?; // link_signer, token, bootstrap_relays, naddr // The crate does no I/O: fetch the naddr from the fragment's relays, then: -let invite = match invite::parse_bundle_event(&event, &link.link_signer, &invite_bundle_key(&link.token))? { +let invite = match cord05::parse_bundle_event(&event, &link.link_signer, &invite_bundle_key(&link.token))? { BundleState::Live(invite) => invite, // validate() already ran BundleState::Revoked => return Ok(None), // a tombstone at the coordinate }; @@ -89,7 +98,7 @@ let invite = match invite::parse_bundle_event(&event, &link.link_signer, &invite A Direct Invite arrives as a NIP-59 gift wrap addressed to the member: ```rust -let (inviter, invite) = invite::unwrap_direct_invite(&wrap, &my_keys)?; +let (inviter, invite) = cord05::unwrap_direct_invite(&wrap, &my_keys)?; ``` Either way the invite carries `community_id`, `owner`, `owner_salt`, @@ -101,26 +110,26 @@ Then publish a join so the member list sees the member before any backfill: ```rust use concord::derive::guestbook_group_key; -use concord::guestbook; +use concord::cord02::guestbook; let guestbook = guestbook_group_key(&invite.community_root, &invite.community_id, invite.root_epoch)?; -let rumor = guestbook::build_join(my_pk, Some((creator_npub, label)), now_ms); -let (wrap, _) = guestbook::seal_rumor(&rumor, &guestbook, &my_keys)?; +let rumor = cord02::guestbook::build_join(my_pk, Some((creator_npub, label)), now_ms); +let (wrap, _) = cord02::guestbook::seal_rumor(&rumor, &guestbook, &my_keys)?; client.send_event(&wrap).to(&relays).await?; ``` ## Reading the Control Plane ```rust -use concord::control::{self, ControlFold}; +use concord::cord02::{self, ControlFold}; let editions: Vec = wraps .iter() - .filter_map(|wrap| control::open_edition(wrap, &read, &control_pk, true).ok()) + .filter_map(|wrap| cord02::open_edition(wrap, &read, &control_pk, true).ok()) .collect(); let control: ControlFold = - control::fold_control(&owner, &community_id, &editions, &state.floors(), &state.banned); + cord02::fold_control(&owner, &community_id, &editions, &state.floors(), &state.banned); state.apply_fold(&control); ``` @@ -145,12 +154,12 @@ fold; they are its memory. ## Sending a message ```rust -use concord::chat::{self, build_message}; +use concord::cord03::{self, build_message}; use concord::derive::channel_group_key; let plane = channel_group_key(&community_root, &channel, epoch)?; // public channel let rumor = build_message(my_pk, &channel, epoch, text, None, at_ms, timer); -let (wrap, wrap_key) = chat::seal_rumor(&rumor, &plane, &my_keys, false)?; +let (wrap, wrap_key) = cord03::seal_rumor(&rumor, &plane, &my_keys, false)?; client.send_event(&wrap).to(&relays).await?; ``` @@ -171,7 +180,7 @@ about an existing `EventId` rather than a mutation. ## Reading a channel ```rust -use concord::chat::{self, fold, plane_keys}; +use concord::cord03::{self, fold, plane_keys}; let planes = plane_keys(&held, &channel)?; // &[(Epoch, secret)] let mut rumors = Vec::new(); @@ -180,7 +189,7 @@ for wrap in &wraps { let Some((epoch, group)) = planes.iter().find(|(_, group)| group.pk() == wrap.pubkey) else { continue; }; - let Ok((opened, rumor)) = chat::open(wrap, group, &channel, *epoch) else { + let Ok((opened, rumor)) = cord03::open(wrap, group, &channel, *epoch) else { continue; }; store::cache_rumor(database, &channel, &opened).await?; @@ -217,11 +226,11 @@ as an inline row only when its author passes ## Membership ```rust -let states = guestbook::coalesce(&rumors, now_ms, Some(&refounder_pk), |actor, target, citation| { +let states = cord02::guestbook::coalesce(&rumors, now_ms, Some(&refounder_pk), |actor, target, citation| { citation_ok(&owner, &community_id, actor, citation, &control.roles.floors) && control.roles.can_act_on_member(actor, &owner, target, Permissions::KICK) }); -let members = guestbook::complete_memberlist(&states, &observed, &granted, &control.banned, &BTreeMap::new()); +let members = cord02::guestbook::complete_memberlist(&states, &observed, &granted, &control.banned, &BTreeMap::new()); ``` - `observed` is npub → ms for every author this client has seen publish anything @@ -254,52 +263,52 @@ and pass the head from the current fold, so the chain cannot silently fork. Wrappers: `set_community_metadata`, `set_channel_metadata`, `set_role`, `set_grant`, `set_banlist`, `set_registry`, `set_pin_list`, plus raw `publish`. A ban is a `set_banlist` followed by a base rekey; a kick is a `set_grant` with an -empty `role_ids` followed by `guestbook::build_kick`. +empty `role_ids` followed by `cord02::guestbook::build_kick`. ## Pins ```rust -use concord::pins; +use concord::cord04::pins; -let entry = pins::build_entry(&opened_message, &plane, &channel)?; +let entry = cord04::pins::build_entry(&opened_message, &plane, &channel)?; let head_content = control.pin_content(&community_id, &channel).unwrap_or(""); -let read = pins::read_list(head_content, |epoch| channel_group_key(&root, &channel, epoch).ok()); -let content = pins::publishable(&read, channel_is_private, &plane, epoch)?; +let read = cord04::pins::read_list(head_content, |epoch| channel_group_key(&root, &channel, epoch).ok()); +let content = cord04::pins::publishable(&read, channel_is_private, &plane, epoch)?; let (wrap, _) = writer.set_pin_list( &my_keys, &community_id, &channel, &content, head, citation, now_secs)?; ``` Reading is verification: `read_list` decodes either content form (public, or sealed under the channel key at the named epoch), and -`pins::verify_entry(entry, &channel)` returns a `VerifiedPin` with the proven +`cord04::pins::verify_entry(entry, &channel)` returns a `VerifiedPin` with the proven author, words and time — no history and no old keys needed. `read.sealed` means the list is sealed under an epoch this client never held: show it as unavailable, -and never write from it (`publishable` refuses). `pins::killed_by(&pin, &delete)` +and never write from it (`publishable` refuses). `cord04::pins::killed_by(&pin, &delete)` answers whether a folded kind-5 erases an entry. ## Invites ```rust use concord::derive::{invite_bundle_key, TOKEN_LEN}; -use concord::invite::{self, InviteEntry, InviteTombstone}; +use concord::cord05::{self, InviteEntry, InviteTombstone}; let token: [u8; TOKEN_LEN] = /* 16 bytes from any CSPRNG */; let bundle_key = invite_bundle_key(&token); let link_signer = Keys::generate(); -let bundle = invite::build_bundle_event(&link_signer, &invite, &bundle_key)?; -let url = invite::build_invite_url(BASE, &link_signer.public_key(), &token, &relays)?; +let bundle = cord05::build_bundle_event(&link_signer, &invite, &bundle_key)?; +let url = cord05::build_invite_url(BASE, &link_signer.public_key(), &token, &relays)?; ``` A link is a coordinate plus a fragment: the naddr fetches the bundle, the token unlocks it, and the fragment names the relays to fetch from. -`invite::stock_relays()` is what a fragment with no relays of its own means. +`cord05::stock_relays()` is what a fragment with no relays of its own means. The `link_signer` secret is what lets the creator refresh or retire the link, so keep it against the token in the member's own Invite List — a local document encrypted to self, exactly like the Community List: ```rust -let mut list = invite::parse_invite_list(&my_keys, &event)?; +let mut list = cord05::parse_invite_list(&my_keys, &event)?; list.entries.push(InviteEntry { token: HEXLOWER.encode(&token), signer_sk: link_signer.secret_key().to_secret_hex(), @@ -310,7 +319,7 @@ list.entries.push(InviteEntry { expires_at: None, extra: Default::default(), }); -let event = invite::build_invite_list(&my_keys, &list)?; // kind 13303 +let event = cord05::build_invite_list(&my_keys, &list)?; // kind 13303 // Retiring is a tombstone, never a deletion: it beats a stale copy terminally. list.tombstones.push(InviteTombstone { @@ -331,10 +340,10 @@ seals one blob per remaining member: ```rust use concord::derive::epoch_key_commitment; -use concord::rekey::{self, RekeyScope}; +use concord::cord06::{self, RekeyScope}; let scope = RekeyScope::Channel(channel_id); // or RekeyScope::Base -let plan = rekey::plan_refounding(Epoch(epoch + 1))?; +let plan = cord06::plan_refounding(Epoch(epoch + 1))?; // A base rotation delivers the new control-plane keys beside the root; a channel // rotation delivers only that channel's fresh key. @@ -350,12 +359,12 @@ let (control_pk, control_root) = match scope { let blobs = members .iter() .map(|member| { - rekey::build_blob(&my_keys, member, scope, plan.epoch, &new_key, control_pk.as_ref(), control_root) + cord06::build_blob(&my_keys, member, scope, plan.epoch, &new_key, control_pk.as_ref(), control_root) }) .collect::, _>>()?; -let rekey_group = rekey::rekey_group(scope, &community_root, &community_id, plan.epoch)?; -let wraps = rekey::build_rekey_chunks( +let rekey_group = cord06::rekey_group(scope, &community_root, &community_id, plan.epoch)?; +let wraps = cord06::build_rekey_chunks( &my_keys, &rekey_group, scope, @@ -369,7 +378,7 @@ let wraps = rekey::build_rekey_chunks( )?; ``` -On the receiving side, `rekey::parse_rekey_chunk(&opened)` per wrap, then +On the receiving side, `cord06::parse_rekey_chunk(&opened)` per wrap, then `collect_rotations(&chunks)`, then `am_i_removed(&rotation, &me)` — which is `None` until every chunk is held, because an incomplete set is never a removal. A member finds their delivery with `find_my_blobs` / `open_blob`, and adopts the key @@ -379,11 +388,11 @@ matches the key they already hold. Two concurrent rotations settle on `fork_winn Dissolution is owner-only and terminal: ```rust -let rumor = rekey::dissolved_tombstone_rumor(owner_pk, &community_id, now_secs); -let wrap = rekey::seal_dissolved(&rumor, &community_id, &my_keys, now_secs)?; +let rumor = cord06::dissolved_tombstone_rumor(owner_pk, &community_id, now_secs); +let wrap = cord06::seal_dissolved(&rumor, &community_id, &my_keys, now_secs)?; // A receiver seals the community read-only on sight. -if rekey::verify_dissolved(&wrap, &identity) { +if cord06::verify_dissolved(&wrap, &identity) { state.dissolved = true; } ``` @@ -393,15 +402,15 @@ if rekey::verify_dissolved(&wrap, &identity) { A member's own memberships, synced across their devices: ```rust -use concord::list; +use concord::cord02::list; -let material = list::join_material(&invite, staff.then_some(&control_root)); -let mut mine = list::parse_list_event(&my_keys, &event)?; -mine = list::merge(mine, list::CommunityList { - entries: vec![list::CommunityListEntry { community_id, seed: material.clone(), current: material, added_at: now_ms, extra: Default::default() }], +let material = cord02::list::join_material(&invite, staff.then_some(&control_root)); +let mut mine = cord02::list::parse_list_event(&my_keys, &event)?; +mine = cord02::list::merge(mine, cord02::list::CommunityList { + entries: vec![cord02::list::CommunityListEntry { community_id, seed: material.clone(), current: material, added_at: now_ms, extra: Default::default() }], ..Default::default() }); -let event = list::build_list_event(&my_keys, &mine)?; // kind 13302, NIP-44 to self +let event = cord02::list::build_list_event(&my_keys, &mine)?; // kind 13302, NIP-44 to self ``` `is_live(&id)` answers joined-versus-left: a tombstone is terminal until a @@ -430,7 +439,7 @@ impl ConcordRegistry { } ``` -Call it after `chat::init` in `desktop/src/main.rs` and `web/src/lib.rs`, and +Call it after `cord03::init` in `desktop/src/main.rs` and `web/src/lib.rs`, and subscribe to `NostrRegistry` for `SignerChanged` so the communities reset with the account. @@ -458,7 +467,7 @@ self.ingress = Some(cx.background_spawn(async move { let Some(plane) = planes.iter().find(|plane| plane.group.pk() == wrap.pubkey) else { continue; }; - let (opened, rumor) = chat::open(wrap, &plane.group, &plane.channel, plane.epoch)?; + let (opened, rumor) = cord03::open(wrap, &plane.group, &plane.channel, plane.epoch)?; store::cache_rumor(database.as_ref(), &plane.channel, &opened).await?; signal_tx.send_async(Signal::Chat { channel: plane.channel, rumor }).await?; }