From 7dda553918705277c7fa5b903c6a40e4b4a0aa8d Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 21 Nov 2025 04:44:40 +0000 Subject: add nip11 --- Cargo.lock | 527 +++++++++++++++++++++++- grasp-audit/Cargo.toml | 4 + grasp-audit/src/client.rs | 19 + grasp-audit/src/specs/grasp01/nip11_document.rs | 189 +++++++-- src/http/mod.rs | 27 ++ src/http/nip11.rs | 146 +++++++ tests/nip11_document.rs | 62 +++ 7 files changed, 923 insertions(+), 51 deletions(-) create mode 100644 src/http/nip11.rs create mode 100644 tests/nip11_document.rs diff --git a/Cargo.lock b/Cargo.lock index 71490b2..5888eb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -225,6 +231,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -380,6 +392,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -446,12 +468,37 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -464,6 +511,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -618,6 +680,8 @@ dependencies = [ "clap", "futures", "nostr-sdk 0.43.0", + "regex", + "reqwest", "serde", "serde_json", "thiserror 1.0.69", @@ -628,6 +692,25 @@ dependencies = [ "uuid", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.12" @@ -639,7 +722,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -689,6 +772,17 @@ dependencies = [ "digest", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.3.1" @@ -700,6 +794,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -707,7 +812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -718,8 +823,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -735,6 +840,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.8.1" @@ -745,9 +874,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", - "http", - "http-body", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -758,6 +887,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.18" @@ -766,9 +908,9 @@ checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "bytes", "futures-core", - "http", - "http-body", - "hyper", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.8.1", "pin-project-lite", "tokio", ] @@ -931,6 +1073,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -965,6 +1113,12 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -1007,6 +1161,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.1.0" @@ -1018,6 +1178,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "negentropy" version = "0.5.0" @@ -1029,12 +1206,12 @@ name = "ngit-grasp" version = "0.1.0" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "dotenvy", "futures-util", "grasp-audit", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "nostr-relay-builder", "nostr-sdk 0.44.1", @@ -1054,7 +1231,7 @@ version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62a97d745f1bd8d5e05a978632bbb87b0614567d5142906fe7c86fb2440faac6" dependencies = [ - "base64", + "base64 0.22.1", "bech32", "bip39", "bitcoin_hashes 0.14.0", @@ -1077,7 +1254,7 @@ version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3595fecf0e0aaacb69a0dc0101a4453f3c76eda333d6bbc49f68f64390b3d85" dependencies = [ - "base64", + "base64 0.22.1", "bech32", "bip39", "bitcoin_hashes 0.14.0", @@ -1242,6 +1419,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1304,6 +1525,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "poly1305" version = "0.8.0" @@ -1422,7 +1649,19 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] @@ -1442,6 +1681,46 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.17.14" @@ -1456,6 +1735,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.34" @@ -1470,6 +1762,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pki-types" version = "1.13.0" @@ -1511,6 +1812,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1549,6 +1859,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -1592,6 +1925,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1650,6 +1995,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.1" @@ -1689,6 +2044,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.13.2" @@ -1700,6 +2061,40 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1786,7 +2181,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", ] @@ -1802,6 +2197,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -1877,6 +2282,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -1952,7 +2363,7 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.3.1", "httparse", "log", "rand 0.9.2", @@ -2047,6 +2458,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2222,6 +2639,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2249,6 +2675,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2282,6 +2723,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2294,6 +2741,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2306,6 +2759,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2330,6 +2789,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2342,6 +2807,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2354,6 +2825,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2366,6 +2843,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2378,6 +2861,16 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/grasp-audit/Cargo.toml b/grasp-audit/Cargo.toml index 278c855..0bc008a 100644 --- a/grasp-audit/Cargo.toml +++ b/grasp-audit/Cargo.toml @@ -33,6 +33,10 @@ clap = { version = "4", features = ["derive"] } uuid = { version = "1", features = ["v4"] } chrono = "0.4" +# HTTP Client +reqwest = { version = "0.11", features = ["json"] } +regex = "1" + # Logging tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs index 019f4cb..35aaccd 100644 --- a/grasp-audit/src/client.rs +++ b/grasp-audit/src/client.rs @@ -84,6 +84,25 @@ impl AuditClient { self.keys.public_key() } + /// Get the relay URL + pub async fn relay_url(&self) -> Result { + let relays = self.client.relays().await; + let relay = relays.values().next() + .ok_or_else(|| anyhow!("No relays configured"))?; + Ok(relay.url().to_string()) + } + + /// Convert WebSocket URL to HTTP(S) URL for NIP-11 requests + pub fn ws_to_http_url(ws_url: &str) -> Result { + if ws_url.starts_with("ws://") { + Ok(ws_url.replace("ws://", "http://")) + } else if ws_url.starts_with("wss://") { + Ok(ws_url.replace("wss://", "https://")) + } else { + Err(anyhow!("Invalid WebSocket URL: {}", ws_url)) + } + } + /// Check if connected to relay pub async fn is_connected(&self) -> bool { // Check if we have any connected relays diff --git a/grasp-audit/src/specs/grasp01/nip11_document.rs b/grasp-audit/src/specs/grasp01/nip11_document.rs index be04777..bb864f2 100644 --- a/grasp-audit/src/specs/grasp01/nip11_document.rs +++ b/grasp-audit/src/specs/grasp01/nip11_document.rs @@ -34,25 +34,53 @@ impl Nip11DocumentTests { /// /// Spec: Line 11 of ../grasp/01.md /// Requirement: MUST serve NIP-11 document - async fn test_nip11_document_exists(_client: &AuditClient) -> TestResult { + pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { TestResult::new( "nip11_document_exists", "GRASP-01:nostr-relay:11", "Serve NIP-11 relay information document", ) .run(|| async { - // TODO: Implementation // 1. Extract HTTP(S) URL from client's WebSocket URL - // - ws://localhost:8081 -> http://localhost:8081 - // - wss://relay.example.com -> https://relay.example.com - // 2. HTTP GET to base URL with header: - // - Accept: application/nostr+json + let ws_url = client.relay_url().await + .map_err(|e| format!("Failed to get relay URL: {}", e))?; + let http_url = AuditClient::ws_to_http_url(&ws_url) + .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; + + // 2. HTTP GET to base URL with Accept: application/nostr+json header + let http_client = reqwest::Client::new(); + let response = http_client + .get(&http_url) + .header("Accept", "application/nostr+json") + .send() + .await + .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?; + // 3. Verify 200 OK response + if !response.status().is_success() { + return Err(format!( + "Expected 200 OK, got {} {}", + response.status().as_u16(), + response.status().canonical_reason().unwrap_or("Unknown") + )); + } + // 4. Verify response is valid JSON - // 5. Parse as NIP-11 document - // 6. Verify has required fields (name, description, etc.) + let json_text = response.text().await + .map_err(|e| format!("Failed to read response body: {}", e))?; + + let doc: serde_json::Value = serde_json::from_str(&json_text) + .map_err(|e| format!("Response is not valid JSON: {}", e))?; - Err("Not implemented yet".to_string()) + // 5. Verify has required NIP-11 fields + let required_fields = ["name", "description", "software", "version"]; + for field in &required_fields { + if !doc.get(field).is_some() { + return Err(format!("Missing required NIP-11 field: {}", field)); + } + } + + Ok(()) }) .await } @@ -61,22 +89,68 @@ impl Nip11DocumentTests { /// /// Spec: Line 12 of ../grasp/01.md /// Requirement: MUST list supported GRASPs as string array - async fn test_nip11_supported_grasps_field(_client: &AuditClient) -> TestResult { + pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { TestResult::new( "nip11_supported_grasps_field", "GRASP-01:nostr-relay:12", "NIP-11 document includes supported_grasps field with GRASP-01", ) .run(|| async { - // TODO: Implementation - // 1. Fetch NIP-11 document (same as above) + // 1. Fetch NIP-11 document + let ws_url = client.relay_url().await + .map_err(|e| format!("Failed to get relay URL: {}", e))?; + let http_url = AuditClient::ws_to_http_url(&ws_url) + .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; + + let http_client = reqwest::Client::new(); + let response = http_client + .get(&http_url) + .header("Accept", "application/nostr+json") + .send() + .await + .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?; + + let json_text = response.text().await + .map_err(|e| format!("Failed to read response body: {}", e))?; + + let doc: serde_json::Value = serde_json::from_str(&json_text) + .map_err(|e| format!("Response is not valid JSON: {}", e))?; + // 2. Verify `supported_grasps` field exists - // 3. Verify it's a JSON array of strings + let supported_grasps = doc.get("supported_grasps") + .ok_or_else(|| "Missing required field: supported_grasps".to_string())?; + + // 3. Verify it's a JSON array + let grasps_array = supported_grasps.as_array() + .ok_or_else(|| "supported_grasps must be an array".to_string())?; + // 4. Verify array includes "GRASP-01" - // 5. Verify format: each entry matches pattern "GRASP-\d{2}" - // 6. Document other GRASPs found (for info) + let grasp_strings: Vec = grasps_array + .iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(); + + if !grasp_strings.contains(&"GRASP-01".to_string()) { + return Err(format!( + "supported_grasps must include 'GRASP-01', found: {:?}", + grasp_strings + )); + } + + // 5. Verify format: each entry should match pattern "GRASP-\d{2}" + let grasp_pattern = regex::Regex::new(r"^GRASP-\d{2}$") + .map_err(|e| format!("Failed to compile regex: {}", e))?; + + for grasp in &grasp_strings { + if !grasp_pattern.is_match(grasp) { + return Err(format!( + "Invalid GRASP format: '{}' (expected GRASP-XX where XX is two digits)", + grasp + )); + } + } - Err("Not implemented yet".to_string()) + Ok(()) }) .await } @@ -85,23 +159,47 @@ impl Nip11DocumentTests { /// /// Spec: Line 13 of ../grasp/01.md /// Requirement: MUST list repository acceptance criteria - async fn test_nip11_repo_acceptance_criteria_field(_client: &AuditClient) -> TestResult { + pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { TestResult::new( "nip11_repo_acceptance_criteria_field", "GRASP-01:nostr-relay:13", "NIP-11 document includes repo_acceptance_criteria field", ) .run(|| async { - // TODO: Implementation // 1. Fetch NIP-11 document + let ws_url = client.relay_url().await + .map_err(|e| format!("Failed to get relay URL: {}", e))?; + let http_url = AuditClient::ws_to_http_url(&ws_url) + .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; + + let http_client = reqwest::Client::new(); + let response = http_client + .get(&http_url) + .header("Accept", "application/nostr+json") + .send() + .await + .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?; + + let json_text = response.text().await + .map_err(|e| format!("Failed to read response body: {}", e))?; + + let doc: serde_json::Value = serde_json::from_str(&json_text) + .map_err(|e| format!("Response is not valid JSON: {}", e))?; + // 2. Verify `repo_acceptance_criteria` field exists - // 3. Verify it's a string (human-readable) + let criteria = doc.get("repo_acceptance_criteria") + .ok_or_else(|| "Missing required field: repo_acceptance_criteria".to_string())?; + + // 3. Verify it's a string + let criteria_str = criteria.as_str() + .ok_or_else(|| "repo_acceptance_criteria must be a string".to_string())?; + // 4. Verify non-empty - // 5. Document the criteria (for info) - // Examples: "Must list this relay in clone and relays tags" - // "Pre-payment required via Lightning invoice" + if criteria_str.trim().is_empty() { + return Err("repo_acceptance_criteria must not be empty".to_string()); + } - Err("Not implemented yet".to_string()) + Ok(()) }) .await } @@ -110,24 +208,47 @@ impl Nip11DocumentTests { /// /// Spec: Line 14 of ../grasp/01.md /// Requirement: MUST include curation if curated, omit otherwise - async fn test_nip11_curation_field(_client: &AuditClient) -> TestResult { + pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { TestResult::new( "nip11_curation_field", "GRASP-01:nostr-relay:14", "NIP-11 curation field present if curated, absent otherwise", ) .run(|| async { - // TODO: Implementation // 1. Fetch NIP-11 document + let ws_url = client.relay_url().await + .map_err(|e| format!("Failed to get relay URL: {}", e))?; + let http_url = AuditClient::ws_to_http_url(&ws_url) + .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; + + let http_client = reqwest::Client::new(); + let response = http_client + .get(&http_url) + .header("Accept", "application/nostr+json") + .send() + .await + .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?; + + let json_text = response.text().await + .map_err(|e| format!("Failed to read response body: {}", e))?; + + let doc: serde_json::Value = serde_json::from_str(&json_text) + .map_err(|e| format!("Response is not valid JSON: {}", e))?; + // 2. Check if `curation` field exists - // 3. If present: - // - Verify it's a non-empty string - // - Document the curation policy - // 4. If absent: - // - Document that no curation beyond SPAM prevention - // 5. Both cases are valid per spec - - Err("Not implemented yet".to_string()) + if let Some(curation) = doc.get("curation") { + // 3. If present: verify it's a non-empty string + let curation_str = curation.as_str() + .ok_or_else(|| "curation field must be a string when present".to_string())?; + + if curation_str.trim().is_empty() { + return Err("curation field must not be empty when present".to_string()); + } + } + // 4. If absent: both cases are valid per spec + + // 5. Both cases are valid - test passes + Ok(()) }) .await } @@ -163,4 +284,4 @@ mod tests { // Don't assert all passed yet - tests not implemented // assert!(results.all_passed(), "Some GRASP-01 NIP-11 document tests failed"); } -} +} \ No newline at end of file diff --git a/src/http/mod.rs b/src/http/mod.rs index 4690790..7c0e7bb 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -2,6 +2,7 @@ /// /// Provides hyper HTTP server with WebSocket upgrade support for the Nostr relay. pub mod landing; +pub mod nip11; use std::future::Future; use std::net::SocketAddr; @@ -46,6 +47,32 @@ impl Service> for HttpService { fn call(&self, req: Request) -> Self::Future { let base = Response::builder().header("server", "ngit-grasp"); + // Check for NIP-11 relay information request (Accept: application/nostr+json) + if let Some(accept) = req.headers().get("accept") { + if accept + .to_str() + .map(|s| s.contains("application/nostr+json")) + .unwrap_or(false) + { + let doc = nip11::RelayInformationDocument::from_config(&self.config); + let json = doc.to_json().unwrap_or_else(|e| { + tracing::error!("Failed to serialize NIP-11 document: {}", e); + "{}".to_string() + }); + + tracing::debug!("Serving NIP-11 relay information document to {}", self.remote); + + return Box::pin(async move { + Ok(base + .status(200) + .header("content-type", "application/nostr+json") + .header("access-control-allow-origin", "*") + .body(json) + .unwrap()) + }); + } + } + // Check if this is a WebSocket upgrade request if let (Some(c), Some(w)) = ( req.headers().get("connection"), diff --git a/src/http/nip11.rs b/src/http/nip11.rs new file mode 100644 index 0000000..a93ee5f --- /dev/null +++ b/src/http/nip11.rs @@ -0,0 +1,146 @@ +/// NIP-11 Relay Information Document +/// +/// Implements NIP-11 relay information endpoint with GRASP-01 extensions. +/// See: https://github.com/nostr-protocol/nips/blob/master/11.md + +use serde::{Deserialize, Serialize}; +use crate::config::Config; + +/// NIP-11 Relay Information Document +/// +/// This structure represents the relay metadata served at the HTTP(S) endpoint +/// when the client sends `Accept: application/nostr+json` header. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RelayInformationDocument { + /// Relay name + pub name: String, + + /// Relay description + pub description: String, + + /// Relay owner's public key (hex format) + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, + + /// Contact information for relay admin + #[serde(skip_serializing_if = "Option::is_none")] + pub contact: Option, + + /// List of NIPs supported by this relay + pub supported_nips: Vec, + + /// Relay software identifier + pub software: String, + + /// Software version + pub version: String, + + // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec) + + /// List of supported GRASPs (e.g., ["GRASP-01"]) + /// Required by GRASP-01 specification line 12 + pub supported_grasps: Vec, + + /// Repository acceptance criteria description + /// Required by GRASP-01 specification line 13 + pub repo_acceptance_criteria: String, + + /// Curation policy (present if curated, absent otherwise) + /// Optional per GRASP-01 specification line 14 + #[serde(skip_serializing_if = "Option::is_none")] + pub curation: Option, +} + +impl RelayInformationDocument { + /// Create NIP-11 relay information document from configuration + pub fn from_config(config: &Config) -> Self { + Self { + name: config.relay_name.clone(), + description: config.relay_description.clone(), + pubkey: Some(config.owner_npub.clone()), + contact: None, // Could be added to config if needed + supported_nips: vec![ + 1, // NIP-01: Basic protocol flow + 11, // NIP-11: Relay information document (this!) + 34, // NIP-34: Git repository announcements + ], + software: env!("CARGO_PKG_NAME").to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + + // GRASP-01 Extensions + supported_grasps: vec!["GRASP-01".to_string()], + repo_acceptance_criteria: format!( + "Repositories must list this relay ({}) in both 'clone' and 'relays' tags of kind 30617 announcements. \ + All other events must reference accepted repositories or accepted events.", + config.domain + ), + curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy + } + } + + /// Serialize to JSON string + pub fn to_json(&self) -> Result { + serde_json::to_string_pretty(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_relay_information_document_structure() { + let config = Config { + domain: "relay.example.com".to_string(), + owner_npub: "npub1test".to_string(), + relay_name: "Test Relay".to_string(), + relay_description: "A test relay".to_string(), + git_data_path: "./data/git".to_string(), + relay_data_path: "./data/relay".to_string(), + bind_address: "127.0.0.1:8080".to_string(), + database_backend: crate::config::DatabaseBackend::Memory, + }; + + let doc = RelayInformationDocument::from_config(&config); + + assert_eq!(doc.name, "Test Relay"); + assert_eq!(doc.description, "A test relay"); + assert_eq!(doc.pubkey, Some("npub1test".to_string())); + assert!(doc.supported_nips.contains(&1)); + assert!(doc.supported_nips.contains(&11)); + assert!(doc.supported_nips.contains(&34)); + assert_eq!(doc.supported_grasps, vec!["GRASP-01"]); + assert!(doc.repo_acceptance_criteria.contains("relay.example.com")); + assert!(doc.curation.is_none()); + } + + #[test] + fn test_relay_information_document_json() { + let config = Config { + domain: "relay.example.com".to_string(), + owner_npub: "npub1test".to_string(), + relay_name: "Test Relay".to_string(), + relay_description: "A test relay".to_string(), + git_data_path: "./data/git".to_string(), + relay_data_path: "./data/relay".to_string(), + bind_address: "127.0.0.1:8080".to_string(), + database_backend: crate::config::DatabaseBackend::Memory, + }; + + let doc = RelayInformationDocument::from_config(&config); + let json = doc.to_json().expect("Failed to serialize to JSON"); + + // Verify JSON contains expected fields + assert!(json.contains("\"name\"")); + assert!(json.contains("\"description\"")); + assert!(json.contains("\"supported_nips\"")); + assert!(json.contains("\"supported_grasps\"")); + assert!(json.contains("\"repo_acceptance_criteria\"")); + assert!(json.contains("GRASP-01")); + + // Verify it's valid JSON by parsing + let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON"); + assert_eq!(parsed["name"], "Test Relay"); + assert_eq!(parsed["supported_grasps"][0], "GRASP-01"); + } +} \ No newline at end of file diff --git a/tests/nip11_document.rs b/tests/nip11_document.rs new file mode 100644 index 0000000..da8e9ce --- /dev/null +++ b/tests/nip11_document.rs @@ -0,0 +1,62 @@ +//! GRASP-01 NIP-11 Document Integration Tests +//! +//! Tests ngit-grasp relay's implementation of GRASP-01 NIP-11 relay information requirements. +//! Uses grasp-audit library to avoid code duplication. +//! +//! # Test Strategy +//! +//! - Each test runs in complete isolation with its own fresh relay instance +//! - Uses macro to eliminate boilerplate while maintaining test isolation +//! - Calls individual test methods from grasp-audit for minimal duplication +//! +//! # Running Tests +//! +//! ```bash +//! # Run all GRASP-01 NIP-11 tests +//! cargo test --test nip11_document +//! +//! # Run specific test +//! cargo test --test nip11_document test_nip11_document_exists +//! +//! # With output +//! cargo test --test nip11_document -- --nocapture +//! ``` + +mod common; + +use common::TestRelay; +use grasp_audit::*; + +/// Macro to generate isolated integration tests +/// +/// Each test runs with its own fresh relay instance to ensure complete isolation. +/// This eliminates rate-limiting issues and ensures tests don't interfere with each other. +macro_rules! isolated_test { + ($test_name:ident) => { + #[tokio::test] + async fn $test_name() { + let relay = TestRelay::start().await; + let config = AuditConfig::ci(); + let client = AuditClient::new(relay.url(), config) + .await + .expect("Failed to create audit client"); + + let result = specs::Nip11DocumentTests::$test_name(&client).await; + + relay.stop().await; + + assert!( + result.passed, + "{} failed: {}", + stringify!($test_name), + result.error.as_deref().unwrap_or("unknown error") + ); + } + }; +} + +// Generate isolated tests for all GRASP-01 NIP-11 document tests +isolated_test!(test_nip11_document_exists); +isolated_test!(test_nip11_supported_grasps_field); +isolated_test!(test_nip11_repo_acceptance_criteria_field); +isolated_test!(test_nip11_curation_field); \ No newline at end of file -- cgit v1.2.3