app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit a72fad6fc8e66e078b91842c0a7e1bbf8a476aa2
parent 4baaf49245ee295e43d2e889684dfafc557f110f
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 19:22:44 +0000

core(nostr): add local relay profile client

- adapt the pinned Nostr SDK behind the application client port
- query configured relays for bounded verified kind-zero events
- prove signed profile exchange through an ephemeral local relay
- keep the relay test lane independent of public network services

Diffstat:
Mcore/Cargo.lock | 649++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcore/Cargo.toml | 6++++++
Mcore/crates/application/Cargo.toml | 6++++++
Mcore/crates/application/src/lib.rs | 2++
Acore/crates/application/src/nostr_client.rs | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
Mdocs/runbooks/local-relay-development.md | 9+++++++--
7 files changed, 790 insertions(+), 22 deletions(-)

diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -3,6 +3,21 @@ version = 4 [[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -102,7 +117,7 @@ dependencies = [ "polling", "rustix", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -160,7 +175,7 @@ dependencies = [ "rustix", "signal-hook-registry", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -181,6 +196,43 @@ dependencies = [ ] [[package]] +name = "async-utility" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9" +dependencies = [ + "futures-util", + "gloo-timers", + "tokio", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-wsocket" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" +dependencies = [ + "async-utility", + "futures", + "futures-util", + "js-sys", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-tungstenite", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "atomic-destructor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" + +[[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -193,6 +245,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -308,6 +375,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -414,11 +487,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] [[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -447,6 +526,12 @@ dependencies = [ ] [[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] name = "endi" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -486,7 +571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -549,6 +634,30 @@ dependencies = [ ] [[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] name = "futures-core" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -585,6 +694,12 @@ dependencies = [ ] [[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] name = "futures-task" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -596,9 +711,13 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -639,6 +758,24 @@ dependencies = [ ] [[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -711,6 +848,22 @@ dependencies = [ ] [[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] name = "icu_collections" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -846,6 +999,17 @@ dependencies = [ ] [[package]] +name = "io-uring" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -919,6 +1083,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + +[[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -934,6 +1104,32 @@ dependencies = [ ] [[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "negentropy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" + +[[package]] name = "nostr" version = "0.44.1" source = "git+https://github.com/rust-nostr/nostr.git?rev=5bba5163eb77107f82c4a8262cf29d7f33a73219#5bba5163eb77107f82c4a8262cf29d7f33a73219" @@ -957,6 +1153,76 @@ dependencies = [ ] [[package]] +name = "nostr-database" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" +dependencies = [ + "lru", + "nostr", + "tokio", +] + +[[package]] +name = "nostr-gossip" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +dependencies = [ + "nostr", +] + +[[package]] +name = "nostr-relay-builder" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ed09e8363503c6f464087b530a1f43cf68be6292bd4928b5ea1141b03dc11c" +dependencies = [ + "async-utility", + "async-wsocket", + "atomic-destructor", + "hex", + "negentropy", + "nostr", + "nostr-database", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-relay-pool" +version = "0.44.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85c54d6ca9aae4ae2bf19a7663ba9db5f45f783f1d24aff55f006386b8b99a1" +dependencies = [ + "async-utility", + "async-wsocket", + "atomic-destructor", + "hex", + "lru", + "negentropy", + "nostr", + "nostr-database", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-sdk" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" +dependencies = [ + "async-utility", + "nostr", + "nostr-database", + "nostr-gossip", + "nostr-relay-pool", + "tokio", + "tracing", +] + +[[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1035,6 +1301,15 @@ dependencies = [ ] [[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1069,7 +1344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1123,7 +1398,7 @@ dependencies = [ "hermit-abi", "pin-project-lite", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1198,9 +1473,13 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" name = "radroots-studio-application" version = "0.1.0-alpha.0" dependencies = [ + "nostr", + "nostr-relay-builder", + "nostr-sdk", "radroots-studio-domain", "radroots-studio-nostr", "secrecy", + "tokio", ] [[package]] @@ -1246,8 +1525,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1257,7 +1546,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1270,6 +1569,15 @@ dependencies = [ ] [[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] name = "refinery" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1291,7 +1599,7 @@ dependencies = [ "regex", "rusqlite", "siphasher", - "thiserror", + "thiserror 2.0.19", "time", "url", "walkdir", @@ -1340,13 +1648,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] name = "rsqlite-vfs" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror", + "thiserror 2.0.19", ] [[package]] @@ -1365,6 +1687,12 @@ dependencies = [ ] [[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1374,7 +1702,41 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1419,7 +1781,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand", + "rand 0.8.7", "secp256k1-sys", "serde", ] @@ -1539,6 +1901,17 @@ dependencies = [ ] [[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1584,6 +1957,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] name = "sqlite-wasm-rs" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1650,7 +2033,16 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] @@ -1659,7 +2051,18 @@ version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1729,6 +2132,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1790,6 +2260,25 @@ dependencies = [ ] [[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.19", + "utf-8", +] + +[[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1803,7 +2292,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1832,6 +2321,12 @@ dependencies = [ ] [[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] name = "url" version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1845,6 +2340,12 @@ dependencies = [ ] [[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1912,6 +2413,16 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "wasm-bindgen-macro" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1954,12 +2465,30 @@ dependencies = [ ] [[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1977,12 +2506,30 @@ dependencies = [ "byteorder", "keyring-core", "regex", - "windows-sys", + "windows-sys 0.61.2", "zeroize", ] [[package]] name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" @@ -1991,6 +2538,70 @@ dependencies = [ ] [[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2062,7 +2673,7 @@ dependencies = [ "tracing", "uds_windows", "uuid", - "windows-sys", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", diff --git a/core/Cargo.toml b/core/Cargo.toml @@ -26,8 +26,14 @@ pedantic = "deny" [workspace.dependencies] keyring = "=4.1.6" nostr = { git = "https://github.com/rust-nostr/nostr.git", rev = "5bba5163eb77107f82c4a8262cf29d7f33a73219", package = "nostr" } +nostr-sdk = "=0.44.1" +nostr-relay-builder = "=0.44.1" refinery = { version = "=0.9.2", default-features = false, features = ["rusqlite"] } rusqlite = { version = "=0.39.0", features = ["bundled"] } secrecy = "=0.10.3" url = "=2.5.8" zeroize = "=1.9.0" +tokio = "=1.47.1" + +[patch.crates-io] +nostr = { git = "https://github.com/rust-nostr/nostr.git", rev = "5bba5163eb77107f82c4a8262cf29d7f33a73219" } diff --git a/core/crates/application/Cargo.toml b/core/crates/application/Cargo.toml @@ -7,9 +7,15 @@ license.workspace = true repository.workspace = true [dependencies] +nostr.workspace = true +nostr-sdk.workspace = true radroots-studio-domain = { path = "../domain" } radroots-studio-nostr = { path = "../nostr" } secrecy.workspace = true +[dev-dependencies] +nostr-relay-builder.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } + [lints] workspace = true diff --git a/core/crates/application/src/lib.rs b/core/crates/application/src/lib.rs @@ -3,6 +3,7 @@ pub mod accounts; pub mod app_core; pub mod config; +pub mod nostr_client; pub mod ports; pub mod recovery; pub mod secrets; @@ -18,6 +19,7 @@ pub use app_core::{AppCore, AppObserver, ObserverHandle, RemovalConfirmationToke pub use config::{ RelayRuntimeMode, relay_configuration_from_environment, relay_configuration_from_value, }; +pub use nostr_client::SdkNostrClient; pub use ports::{ AccountNamespaceRepository, AccountOperationKind, AccountOperationPhase, AccountPreferenceKey, AccountRepository, AppStateRepository, BoxFuture, CachedProfile, Clock, NostrClient, diff --git a/core/crates/application/src/nostr_client.rs b/core/crates/application/src/nostr_client.rs @@ -0,0 +1,138 @@ +use std::time::Duration; + +use nostr::{Filter, JsonUtil, Kind, PublicKey as NostrPublicKey}; +use nostr_sdk::ClientBuilder; +use radroots_studio_domain::{ + Kind0ProfileCandidate, PublicKey, RelayUrl, SafeError, SafeErrorCode, SafeMessage, + select_latest_kind0, +}; + +use crate::{BoxFuture, NostrClient}; + +pub struct SdkNostrClient { + timeout: Duration, +} + +impl SdkNostrClient { + #[must_use] + pub const fn new(timeout: Duration) -> Self { + Self { timeout } + } +} + +impl NostrClient for SdkNostrClient { + fn fetch_profile<'a>( + &'a self, + public_key: PublicKey, + relays: &'a [RelayUrl], + ) -> BoxFuture<'a, Result<Option<Kind0ProfileCandidate>, SafeError>> { + Box::pin(async move { + if relays.is_empty() { + return Err(invalid_relay_configuration()); + } + + let client = ClientBuilder::new().build(); + for relay in relays { + client + .add_relay(relay.as_str()) + .await + .map_err(|_| relay_connection_failed())?; + } + client.connect().await; + client.wait_for_connection(self.timeout).await; + + let author = NostrPublicKey::from_slice(public_key.as_bytes()) + .map_err(|_| profile_refresh_failed())?; + let filter = Filter::new().author(author).kind(Kind::Metadata).limit(64); + let fetched = client.fetch_events(filter, self.timeout).await; + client.shutdown().await; + let events = fetched.map_err(|_| relay_connection_failed())?; + + let mut candidates = Vec::with_capacity(events.len()); + for event in events.iter() { + candidates.push(radroots_studio_nostr::parse_verified_kind0( + &event.as_json(), + public_key, + )?); + } + Ok(select_latest_kind0(candidates)) + }) + } +} + +const fn invalid_relay_configuration() -> SafeError { + SafeError::new( + SafeErrorCode::InvalidRelayConfiguration, + SafeMessage::new("No Nostr relay is configured."), + ) +} + +const fn relay_connection_failed() -> SafeError { + SafeError::new( + SafeErrorCode::RelayConnectionFailed, + SafeMessage::new("The Nostr relays could not be reached."), + ) +} + +const fn profile_refresh_failed() -> SafeError { + SafeError::new( + SafeErrorCode::ProfileRefreshFailed, + SafeMessage::new("The Nostr profile could not be refreshed."), + ) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use nostr::{EventBuilder, Keys, Metadata}; + use nostr_relay_builder::MockRelay; + use nostr_sdk::Client; + use radroots_studio_domain::{PublicKey, RelayUrl, SafeErrorCode}; + + use crate::{NostrClient, SdkNostrClient}; + + #[tokio::test] + async fn sdk_client_fetches_verified_profile_from_ephemeral_local_relay() { + let relay = MockRelay::run().await.expect("local relay"); + let relay_url = relay.url().await; + let keys = Keys::generate(); + let publisher = Client::new(keys.clone()); + publisher + .add_relay(relay_url.clone()) + .await + .expect("add relay"); + publisher.connect().await; + publisher.wait_for_connection(Duration::from_secs(2)).await; + publisher + .send_event_builder(EventBuilder::metadata( + &Metadata::new().name("Farmer").display_name("Farm Account"), + )) + .await + .expect("publish metadata"); + + let adapter = SdkNostrClient::new(Duration::from_secs(2)); + let domain_relay = RelayUrl::parse(relay_url.as_str()).expect("domain relay URL"); + let public_key = PublicKey::from_bytes(keys.public_key().to_bytes()); + let profile = adapter + .fetch_profile(public_key, &[domain_relay]) + .await + .expect("fetch profile") + .expect("published profile"); + + assert_eq!(profile.author(), public_key); + assert_eq!(profile.metadata().preferred_name(), Some("Farm Account")); + publisher.shutdown().await; + relay.shutdown(); + } + + #[tokio::test] + async fn sdk_client_rejects_empty_configuration_without_network_access() { + let error = SdkNostrClient::new(Duration::from_millis(10)) + .fetch_profile(PublicKey::from_bytes([1; 32]), &[]) + .await + .expect_err("empty relay list"); + + assert_eq!(error.code(), SafeErrorCode::InvalidRelayConfiguration); + } +} diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -515,7 +515,7 @@ handoff commit sequence. - [x] 36. Add relay configuration source and environment parser. - [x] 37. Implement Nostr event verification and kind-0 parsing adapter. -- [ ] 38. Add Nostr client port implementation and local relay fixture. +- [x] 38. Add Nostr client port implementation and local relay fixture. - [ ] 39. Implement cache-first active profile refresh orchestration. - [ ] 40. Expose manual refreshActiveProfile command. diff --git a/docs/runbooks/local-relay-development.md b/docs/runbooks/local-relay-development.md @@ -7,9 +7,14 @@ deduplicates while preserving first-seen order. Development and tests may use have no fallback and report `InvalidRelayConfiguration` until at least one valid relay is configured. -## Status +The Rust integration lane starts an in-process relay on an ephemeral loopback +port, publishes a signed kind-0 event, fetches it through the production SDK +adapter, and shuts both clients and the relay down before returning. Run it +without any public-network dependency: -Initial runbook contract. Exact commands will be added with the relay fixture. +```sh +cargo test --manifest-path core/Cargo.toml -p radroots-studio-application sdk_client +``` Development and tests use local WebSocket relays only. The default development endpoint is `ws://localhost:8080`; tests bind ephemeral loopback ports. Override