diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index a58e57b..85357b4 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -21,6 +21,40 @@ jobs: components: rustfmt, clippy override: true + - name: Cache cargo registry + uses: actions/cache@v2 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + - name: Cache cargo index + uses: actions/cache@v2 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-index- + - name: Cache cargo build + id: cargo-build-cache + uses: actions/cache@v2 + with: + path: target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build- + - name: Cache Nextest installation + uses: actions/cache@v2 + with: + path: ~/.cargo/bin/cargo-nextest + key: ${{ runner.os }}-cargo-nextest + + - name: Install Nextest if not cached + run: | + if [ ! -f ~/.cargo/bin/cargo-nextest ]; then + cargo install cargo-nextest + fi + - name: Check no default features run: cargo check --no-default-features @@ -31,10 +65,11 @@ jobs: run: cargo clippy --workspace --all-features --bins --tests - name: Build + if: steps.cargo-build-cache.outputs.cache-hit != 'true' run: cargo build --release --workspace --all-features --verbose - - name: Run tests - run: cargo test --all-features --workspace --verbose + - name: Run tests with Nextest + run: cargo nextest run --all-features --workspace --verbose - name: Run docs run: cargo doc --workspace --all-features --no-deps --document-private-items --verbose diff --git a/Cargo.lock b/Cargo.lock index 26bae45..a712105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,12 +36,73 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -64,6 +135,20 @@ dependencies = [ "critical-section", ] +[[package]] +name = "authenticator" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "clap", + "pubky", + "pubky-common", + "rpassword", + "tokio", + "url", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -78,6 +163,7 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http", @@ -142,12 +228,26 @@ dependencies = [ "mime", "pin-project-lite", "serde", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -205,6 +305,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "blake3" version = "1.5.2" @@ -241,9 +350,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" @@ -257,12 +366,69 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "cobs" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "const-oid" version = "0.9.6" @@ -293,9 +459,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" dependencies = [ "cookie", - "idna", - "indexmap", + "idna 0.5.0", "log", + "publicsuffix", "serde", "serde_derive", "serde_json", @@ -327,15 +493,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - [[package]] name = "critical-section" version = "1.1.2" @@ -364,9 +521,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -421,6 +594,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -475,6 +649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", + "serde", "signature", ] @@ -511,16 +686,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "flate2" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.0" @@ -645,6 +810,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -719,6 +885,18 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "heed" version = "0.20.3" @@ -763,6 +941,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.1.0" @@ -826,6 +1010,25 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", ] [[package]] @@ -835,12 +1038,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -855,14 +1073,35 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -871,9 +1110,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -997,13 +1236,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1031,16 +1271,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.36.1" @@ -1056,6 +1286,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "overload" version = "0.1.1" @@ -1095,6 +1331,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1177,10 +1424,10 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4548c673cbf8c91b69f7a17d3a042710aa73cffe5e82351db5378f26c3be64d8" +version = "2.2.0" +source = "git+https://github.com/Pubky/pkarr?branch=v3#17975121c809d97dcad907fbb2ffc782e994d270" dependencies = [ + "base32", "bytes", "document-features", "dyn-clone", @@ -1192,13 +1439,13 @@ dependencies = [ "mainline", "rand", "self_cell", + "serde", "simple-dns", "thiserror", "tracing", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "z32", ] [[package]] @@ -1211,6 +1458,17 @@ dependencies = [ "spki", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "postcard" version = "1.0.8" @@ -1244,27 +1502,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "pubky" version = "0.1.0" dependencies = [ - "flume", + "base64 0.22.1", + "bytes", + "js-sys", "pkarr", "pubky-common", "pubky_homeserver", + "reqwest", "thiserror", "tokio", - "ureq", "url", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] name = "pubky-common" version = "0.1.0" dependencies = [ + "argon2", "base32", "blake3", + "crypto_secretbox", "ed25519-dalek", + "js-sys", "once_cell", "pkarr", "postcard", @@ -1282,19 +1553,82 @@ dependencies = [ "axum-extra", "base32", "bytes", + "clap", "dirs-next", + "flume", + "futures-util", "heed", + "hex", "pkarr", "postcard", "pubky-common", "serde", "tokio", + "toml", "tower-cookies", "tower-http", "tracing", "tracing-subscriber", ] +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quinn" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -1398,6 +1732,50 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + [[package]] name = "ring" version = "0.17.8" @@ -1413,12 +1791,39 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1430,11 +1835,10 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.11" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -1444,16 +1848,26 @@ dependencies = [ ] [[package]] -name = "rustls-pki-types" -version = "1.7.0" +name = "rustls-pemfile" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "ring", "rustls-pki-types", @@ -1472,6 +1886,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1492,9 +1915,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -1520,9 +1943,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -1550,6 +1973,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1682,6 +2114,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1798,34 +2236,91 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1950,6 +2445,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -1977,30 +2478,22 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" -dependencies = [ - "base64 0.22.1", - "cookie", - "cookie_store", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots", -] - [[package]] name = "url" version = "2.5.2" @@ -2008,10 +2501,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" @@ -2024,6 +2523,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2032,19 +2540,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -2069,9 +2578,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2079,9 +2588,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -2092,15 +2601,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -2277,10 +2786,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "z32" -version = "1.1.1" +name = "winnow" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] [[package]] name = "zeroize" diff --git a/Cargo.toml b/Cargo.toml index 85e44a1..8514809 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,18 @@ [workspace] -members = [ "pubky","pubky-*"] +members = [ + "pubky", + "pubky-*", + + "examples/authz/authenticator" +] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" + +[workspace.dependencies] +pkarr = { git = "https://github.com/Pubky/pkarr", branch = "v3", package = "pkarr", features = ["async"] } +serde = { version = "^1.0.209", features = ["derive"] } + +[profile.release] +lto = true +opt-level = 'z' diff --git a/README.md b/README.md index 99e127f..23197ae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # Pubky -Public key addressable web. +> The Web, long centralized, must decentralize; Long decentralized, must centralize. + +> [!WARNING] +> Pubky is still under heavy development and should be considered an alpha software. +> +> Features might be added, removed, or changed. Data might be lost. diff --git a/examples/authz/3rd-party-app/.gitignore b/examples/authz/3rd-party-app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/examples/authz/3rd-party-app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html new file mode 100644 index 0000000..1249852 --- /dev/null +++ b/examples/authz/3rd-party-app/index.html @@ -0,0 +1,26 @@ + + + + + + + Pubky Auth Demo + + + + + + + + +
+

Third Party app!

+

this is a demo for using Pubky Auth in an unhosted (no backend) app.

+
+ + diff --git a/examples/authz/3rd-party-app/package-lock.json b/examples/authz/3rd-party-app/package-lock.json new file mode 100644 index 0000000..98ba42f --- /dev/null +++ b/examples/authz/3rd-party-app/package-lock.json @@ -0,0 +1,1146 @@ +{ + "name": "pubky-auth-3rd-party", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pubky-auth-3rd-party", + "version": "0.0.0", + "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", + "lit": "^3.2.0", + "qrcode": "^1.5.4" + }, + "devDependencies": { + "vite": "^5.4.2" + } + }, + "../../../pubky/pkg": { + "name": "@synonymdev/pubky", + "version": "0.1.14", + "license": "MIT", + "devDependencies": { + "browser-resolve": "^2.0.0", + "esmify": "^2.1.1", + "tape": "^5.8.1", + "tape-run": "^11.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", + "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==" + }, + "node_modules/@lit/reactive-element": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", + "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", + "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", + "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", + "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", + "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", + "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", + "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", + "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", + "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", + "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", + "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", + "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", + "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", + "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", + "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", + "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@synonymdev/pubky": { + "resolved": "../../../pubky/pkg", + "link": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lit": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz", + "integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==", + "dependencies": { + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.1.0", + "lit-html": "^3.2.0" + } + }, + "node_modules/lit-element": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.0.tgz", + "integrity": "sha512-gSejRUQJuMQjV2Z59KAS/D4iElUhwKpIyJvZ9w+DIagIQjfJnhR20h2Q5ddpzXGS+fF0tMZ/xEYGMnKmaI/iww==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.2.0" + } + }, + "node_modules/lit-html": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.0.tgz", + "integrity": "sha512-pwT/HwoxqI9FggTrYVarkBKFN9MlTUpLrDHubTmW4SrkL3kkqW5gxwbxMMUnbbRHBC0WTZnYHcjDSCM559VyfA==", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.4.42", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.42.tgz", + "integrity": "sha512-hywKUQB9Ra4dR1mGhldy5Aj1X3MWDSIA1cEi+Uy0CjheLvP6Ual5RlwMCh8i/X121yEDLDIKBsrCQ8ba3FDMfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/rollup": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", + "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.2", + "@rollup/rollup-android-arm64": "4.21.2", + "@rollup/rollup-darwin-arm64": "4.21.2", + "@rollup/rollup-darwin-x64": "4.21.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", + "@rollup/rollup-linux-arm-musleabihf": "4.21.2", + "@rollup/rollup-linux-arm64-gnu": "4.21.2", + "@rollup/rollup-linux-arm64-musl": "4.21.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", + "@rollup/rollup-linux-riscv64-gnu": "4.21.2", + "@rollup/rollup-linux-s390x-gnu": "4.21.2", + "@rollup/rollup-linux-x64-gnu": "4.21.2", + "@rollup/rollup-linux-x64-musl": "4.21.2", + "@rollup/rollup-win32-arm64-msvc": "4.21.2", + "@rollup/rollup-win32-ia32-msvc": "4.21.2", + "@rollup/rollup-win32-x64-msvc": "4.21.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vite": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/examples/authz/3rd-party-app/package.json b/examples/authz/3rd-party-app/package.json new file mode 100644 index 0000000..8ccad30 --- /dev/null +++ b/examples/authz/3rd-party-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "pubky-auth-3rd-party", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "npm run dev", + "dev": "vite --host --open", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", + "lit": "^3.2.0", + "qrcode": "^1.5.4" + }, + "devDependencies": { + "vite": "^5.4.2" + } +} diff --git a/examples/authz/3rd-party-app/public/pubky.svg b/examples/authz/3rd-party-app/public/pubky.svg new file mode 100644 index 0000000..6802915 --- /dev/null +++ b/examples/authz/3rd-party-app/public/pubky.svg @@ -0,0 +1 @@ + diff --git a/examples/authz/3rd-party-app/src/index.css b/examples/authz/3rd-party-app/src/index.css new file mode 100644 index 0000000..809fde4 --- /dev/null +++ b/examples/authz/3rd-party-app/src/index.css @@ -0,0 +1,48 @@ +:root { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: white; + + background: radial-gradient( + circle, + transparent 20%, + #151718 20%, + #151718 80%, + transparent 80%, + transparent + ), + radial-gradient( + circle, + transparent 20%, + #151718 20%, + #151718 80%, + transparent 80%, + transparent + ) + 25px 25px, + linear-gradient(#202020 1px, transparent 2px) 0 -1px, + linear-gradient(90deg, #202020 1px, #151718 2px) -1px 0; + background-size: 50px 50px, 50px 50px, 25px 25px, 25px 25px; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 20rem; + min-height: 100vh; + font-family: var(--font-family); +} + +h1 { + font-weight: bold; + font-size: 3.2rem; + line-height: 1.1; +} + +main { + max-width: 80rem; + margin: 0 auto; + padding: 2rem; + text-align: center; +} diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js new file mode 100644 index 0000000..2070f82 --- /dev/null +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -0,0 +1,336 @@ +import { LitElement, css, html } from 'lit' +import { createRef, ref } from 'lit/directives/ref.js'; +import QRCode from 'qrcode' + +const DEFAULT_HTTP_RELAY = "https://demo.httprelay.io/link" + +/** + */ +export class PubkyAuthWidget extends LitElement { + static get properties() { + return { + /** + * Relay endpoint for the widget to receive Pubky AuthTokens + * + * Internally, a random channel ID will be generated and a + * GET request made for `${realy url}/${channelID}` + * + * If no relay is passed, the widget will use a default relay: + * https://demo.httprelay.io/link + */ + relay: { type: String }, + /** + * Capabilities requested or this application encoded as a string. + */ + caps: { type: String }, + /** + * Widget's state (open or closed) + */ + open: { type: Boolean }, + /** + * Show "copied to clipboard" note + */ + showCopied: { type: Boolean }, + } + } + + canvasRef = createRef(); + + constructor() { + if (!window.pubky) { + throw new Error("window.pubky is unavailable, make sure to import `@synonymdev/pubky` before this web component.") + } + + super() + + this.open = false; + + // TODO: allow using mainnet + /** @type {import("@synonymdev/pubky").PubkyClient} */ + this.pubkyClient = window.pubky.PubkyClient.testnet(); + } + + connectedCallback() { + super.connectedCallback() + + let [url, promise] = this.pubkyClient.authRequest(this.relay || DEFAULT_HTTP_RELAY, this.caps); + + promise.then(session => { + console.log({ id: session.pubky().z32(), capabilities: session.capabilities() }) + alert(`Successfully signed in to ${session.pubky().z32()} with capabilities: ${session.capabilities().join(",")}`) + }).catch(e => { + console.error(e) + }) + + // let keypair = pubky.Keypair.random(); + // const Homeserver = pubky.PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + // this.pubkyClient.signup(keypair, Homeserver).then(() => { + // this.pubkyClient.sendAuthToken(keypair, url) + // }) + + this.authUrl = url + } + + render() { + return html` +
+ +
+
+

Scan or copy Pubky auth URL

+
+ +
+ +
+
+ ` + } + + _setQr(canvas) { + QRCode.toCanvas(canvas, this.authUrl, { + margin: 2, + scale: 8, + + color: { + light: '#fff', + dark: '#000', + }, + }); + } + + _switchOpen() { + this.open = !this.open + } + + async _copyToClipboard() { + try { + await navigator.clipboard.writeText(this.authUrl); + this.showCopied = true; + setTimeout(() => { this.showCopied = false }, 1000) + } catch (error) { + console.error('Failed to copy text: ', error); + } + } + + + + render() { + return html` +
+ +
+
+

Scan or copy Pubky auth URL

+
+ +
+ +
+
+ ` + } + + static get styles() { + return css` + * { + box-sizing: border-box; + } + + :host { + --full-width: 22rem; + --full-height: 31rem; + --header-height: 3rem; + --closed-width: 3rem; + } + + a { + text-decoration: none; + } + + button { + padding: 0; + background: none; + border: none; + color: inherit; + cursor: pointer; + } + + p { + margin: 0; + } + + /** End reset */ + + #widget { + color: white; + + position: fixed; + top: 1rem; + right: 1rem; + + background-color:red; + + z-index: 99999; + overflow: hidden; + background: rgba(43, 43, 43, .7372549019607844); + border: 1px solid #3c3c3c; + box-shadow: 0 10px 34px -10px rgba(236, 243, 222, .05); + border-radius: 8px; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + + width: var(--closed-width); + height: var(--header-height); + + will-change: height,width; + transition-property: height, width; + transition-duration: 80ms; + transition-timing-function: ease-in; + } + + #widget.open{ + width: var(--full-width); + height: var(--full-height); + } + + .header { + height: var(--header-height); + display: flex; + justify-content: center; + align-items: center; + } + + #widget + .header .text { + display: none; + font-weight: bold; + } + #widget.open + .header .text { + display: block + } + + #widget.open + .header { + width: var(--full-width); + justify-content: center; + } + + #pubky-icon { + height: 100%; + width: 100%; + } + + #widget.open + #pubky-icon { + width: var(--header-height); + height: 74%; + } + + #widget-content{ + width: var(--full-width); + padding: 0 1rem + } + + #widget p { + font-size: .87rem; + line-height: 1rem; + text-align: center; + color: #fff; + opacity: .5; + + /* Fix flash wrap in open animation */ + text-wrap: nowrap; + } + + #qr { + width: 18em !important; + height: 18em !important; + } + + .card { + position: relative; + background: #3b3b3b; + border-radius: 5px; + padding: 1rem; + margin-top: 1rem; + display: flex; + justify-content: center; + align-items: center; + } + + .card.url { + padding: .625rem; + justify-content: space-between; + max-width:100%; + } + + .url p { + display: flex; + align-items: center; + + line-height: 1!important; + width: 93%; + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; + } + + .line { + height: 1px; + background-color: #3b3b3b; + flex: 1 1; + margin-bottom: 1rem; + } + + .copied { + will-change: opacity; + transition-property: opacity; + transition-duration: 80ms; + transition-timing-function: ease-in; + + opacity: 0; + + position: absolute; + right: 0; + top: -1.6rem; + font-size: 0.9em; + background: rgb(43 43 43 / 98%); + padding: .5rem; + border-radius: .3rem; + color: #ddd; + } + + .copied.show { + opacity:1 + } + ` + } +} + +window.customElements.define('pubky-auth-widget', PubkyAuthWidget) diff --git a/examples/authz/README.md b/examples/authz/README.md new file mode 100644 index 0000000..905bda6 --- /dev/null +++ b/examples/authz/README.md @@ -0,0 +1,29 @@ +# Pubky Auth Example + +This example shows 3rd party authorization in Pubky. + +It consists of 2 parts: + +1. [3rd party app](./3rd-party-app): A web component showing the how to implement a Pubky Auth widget. +2. [Authenticator CLI](./authenticator): A CLI showing the authenticator (key chain) asking user for consent and generating the AuthToken. + +## Usage + +First you need to be running a local testnet Homeserver, in the root of this repo run + +```bash +cargo run --bin pubky_homeserver -- --testnet +``` + +Run the frontend of the 3rd party app + +```bash +cd ./3rd-party-app +npm start +``` + +Copy the Pubky Auth URL from the frontend. + +Finally run the CLI to paste the Pubky Auth in. + +You should see the frontend reacting by showing the success of authorization and session details. diff --git a/examples/authz/authenticator/Cargo.lock b/examples/authz/authenticator/Cargo.lock new file mode 100644 index 0000000..f2fe8b2 --- /dev/null +++ b/examples/authz/authenticator/Cargo.lock @@ -0,0 +1,1906 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "authenticator" +version = "0.1.0" +dependencies = [ + "anyhow", + "keyring", + "pubky", + "pubky-common", + "rpassword", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +dependencies = [ + "cookie", + "idna 0.5.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "critical-section" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64009896348fc5af4222e9cf7d7d82a95a256c634ebcf61c53e4ea461422242" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b9af47ded4df3067484d7d45758ca2b36bd083bf6d024c2952bbd8af1cdaa4" +dependencies = [ + "byteorder", + "linux-keyutils", + "security-framework", + "windows-sys 0.59.0", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" + +[[package]] +name = "mainline" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" +dependencies = [ + "bytes", + "crc", + "ed25519-dalek", + "flume", + "lru", + "rand", + "serde", + "serde_bencode", + "serde_bytes", + "sha1_smol", + "thiserror", + "tracing", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkarr" +version = "2.2.0" +source = "git+https://github.com/Pubky/pkarr?branch=v3#17975121c809d97dcad907fbb2ffc782e994d270" +dependencies = [ + "base32", + "bytes", + "document-features", + "dyn-clone", + "ed25519-dalek", + "flume", + "futures", + "js-sys", + "lru", + "mainline", + "rand", + "self_cell", + "serde", + "simple-dns", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postcard" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "pubky" +version = "0.1.0" +dependencies = [ + "argon2", + "bytes", + "js-sys", + "pkarr", + "pubky-common", + "reqwest", + "thiserror", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "pubky-common" +version = "0.1.0" +dependencies = [ + "base32", + "blake3", + "crypto_secretbox", + "ed25519-dalek", + "js-sys", + "once_cell", + "pkarr", + "postcard", + "rand", + "serde", + "thiserror", +] + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bencode" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" +dependencies = [ + "serde", + "serde_bytes", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[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_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simple-dns" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" +dependencies = [ + "bitflags", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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" +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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/examples/authz/authenticator/Cargo.toml b/examples/authz/authenticator/Cargo.toml new file mode 100644 index 0000000..932701b --- /dev/null +++ b/examples/authz/authenticator/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "authenticator" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +base64 = "0.22.1" +clap = { version = "4.5.16", features = ["derive"] } +pubky = { version = "0.1.0", path = "../../../pubky" } +pubky-common = { version = "0.1.0", path = "../../../pubky-common" } +rpassword = "7.3.1" +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } +url = "2.5.2" diff --git a/examples/authz/authenticator/src/main.rs b/examples/authz/authenticator/src/main.rs new file mode 100644 index 0000000..410b8f5 --- /dev/null +++ b/examples/authz/authenticator/src/main.rs @@ -0,0 +1,80 @@ +use anyhow::Result; +use clap::Parser; +use pubky::PubkyClient; +use std::path::PathBuf; +use url::Url; + +use pubky_common::{capabilities::Capability, crypto::PublicKey}; + +/// local testnet HOMESERVER +const HOMESERVER: &str = "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Cli { + /// Path to a recovery_file of the Pubky you want to sign in with + recovery_file: PathBuf, + + /// Pubky Auth url + url: Url, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + let recovery_file = std::fs::read(&cli.recovery_file)?; + println!("\nSuccessfully opened recovery file"); + + let url = cli.url; + + let caps = url + .query_pairs() + .filter_map(|(key, value)| { + if key == "caps" { + return Some( + value + .split(',') + .filter_map(|cap| Capability::try_from(cap).ok()) + .collect::>(), + ); + }; + None + }) + .next() + .unwrap_or_default(); + + if !caps.is_empty() { + println!("\nRequired Capabilities:"); + } + + for cap in &caps { + println!(" {} : {:?}", cap.scope, cap.actions); + } + + // === Consent form === + + println!("\nEnter your recovery_file's passphrase to confirm:"); + let passphrase = rpassword::read_password()?; + + let keypair = pubky_common::recovery_file::decrypt_recovery_file(&recovery_file, &passphrase)?; + + println!("Successfully decrypted recovery file..."); + println!("PublicKey: {}", keypair.public_key()); + + let client = PubkyClient::testnet(); + + // For the purposes of this demo, we need to make sure + // the user has an account on the local homeserver. + if client.signin(&keypair).await.is_err() { + client + .signup(&keypair, &PublicKey::try_from(HOMESERVER).unwrap()) + .await?; + }; + + println!("Sending AuthToken to the 3rd party app..."); + + client.send_auth_token(&keypair, url).await?; + + Ok(()) +} diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 1b7111c..9676fba 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -10,8 +10,24 @@ base32 = "0.5.0" blake3 = "1.5.1" ed25519-dalek = "2.1.1" once_cell = "1.19.0" -pkarr = "2.1.0" +pkarr = { workspace = true } rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } -serde = { version = "1.0.204", features = ["derive"] } +crypto_secretbox = { version = "0.1.1", features = ["std"] } +argon2 = { version = "0.5.3", features = ["std"] } + +serde = { workspace = true, optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3.69" + +[dev-dependencies] +postcard = "1.0.8" + +[features] + +serde = ["dep:serde", "ed25519-dalek/serde", "pkarr/serde"] +full = ['serde'] + +default = ['full'] diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 7fc2a02..866fe5e 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -2,104 +2,153 @@ use std::sync::{Arc, Mutex}; -use ed25519_dalek::ed25519::SignatureBytes; +use serde::{Deserialize, Serialize}; use crate::{ - crypto::{random_hash, Keypair, PublicKey, Signature}, + capabilities::{Capabilities, Capability}, + crypto::{Keypair, PublicKey, Signature}, + namespaces::PUBKY_AUTH, timestamp::Timestamp, }; // 30 seconds const TIME_INTERVAL: u64 = 30 * 1_000_000; -#[derive(Debug, PartialEq)] -pub struct AuthnSignature(Box<[u8]>); +const CURRENT_VERSION: u8 = 0; +// 45 seconds in the past or the future +const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000; -impl AuthnSignature { - pub fn new(signer: &Keypair, audience: &PublicKey, token: Option<&[u8]>) -> Self { - let mut bytes = Vec::with_capacity(96); - - let time: u64 = Timestamp::now().into(); - let time_step = time / TIME_INTERVAL; - - let token_hash = token.map_or(random_hash(), crate::crypto::hash); - - let signature = signer - .sign(&signable( - &time_step.to_be_bytes(), - &signer.public_key(), - audience, - token_hash.as_bytes(), - )) - .to_bytes(); - - bytes.extend_from_slice(&signature); - bytes.extend_from_slice(token_hash.as_bytes()); - - Self(bytes.into()) - } - - /// Sign a randomly generated nonce - pub fn generate(keypair: &Keypair, audience: &PublicKey) -> Self { - AuthnSignature::new(keypair, audience, None) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct AuthToken { + /// Signature over the token. + signature: Signature, + /// A namespace to ensure this signature can't be used for any + /// other purposes that share the same message structurea by accident. + namespace: [u8; 10], + /// Version of the [AuthToken], in case we need to upgrade it to support unforseen usecases. + /// + /// Version 0: + /// - Signer is implicitly the same as the root keypair for + /// the [AuthToken::pubky], without any delegation. + /// - Capabilities are only meant for resoucres on the homeserver. + version: u8, + /// Timestamp + timestamp: Timestamp, + /// The [PublicKey] of the owner of the resources being accessed by this token. + pubky: PublicKey, + // Variable length capabilities + capabilities: Capabilities, } -#[derive(Debug, Clone)] -pub struct AuthnVerifier { - audience: PublicKey, - inner: Arc>>, - // TODO: Support permisisons - // token_hashes: HashSet<[u8; 32]>, -} +impl AuthToken { + pub fn sign(keypair: &Keypair, capabilities: impl Into) -> Self { + let timestamp = Timestamp::now(); -impl AuthnVerifier { - pub fn new(audience: PublicKey) -> Self { - Self { - audience, - inner: Arc::new(Mutex::new(Vec::new())), + let mut token = Self { + signature: Signature::from_bytes(&[0; 64]), + namespace: *PUBKY_AUTH, + version: 0, + timestamp, + pubky: keypair.public_key(), + capabilities: capabilities.into(), + }; + + let serialized = token.serialize(); + + token.signature = keypair.sign(&serialized[65..]); + + token + } + + pub fn capabilities(&self) -> &[Capability] { + &self.capabilities.0 + } + + pub fn verify(bytes: &[u8]) -> Result { + if bytes[75] > CURRENT_VERSION { + return Err(Error::UnknownVersion); + } + + let token = AuthToken::deserialize(bytes)?; + + match token.version { + 0 => { + let now = Timestamp::now(); + + // Chcek timestamp; + let diff = token.timestamp.difference(&now); + if diff > TIMESTAMP_WINDOW { + return Err(Error::TooFarInTheFuture); + } + if diff < -TIMESTAMP_WINDOW { + return Err(Error::Expired); + } + + token + .pubky + .verify(AuthToken::signable(token.version, bytes), &token.signature) + .map_err(|_| Error::InvalidSignature)?; + + Ok(token) + } + _ => unreachable!(), } } - pub fn verify(&self, bytes: &[u8], signer: &PublicKey) -> Result<(), AuthnSignatureError> { + pub fn serialize(&self) -> Vec { + postcard::to_allocvec(self).unwrap() + } + + pub fn deserialize(bytes: &[u8]) -> Result { + Ok(postcard::from_bytes(bytes)?) + } + + pub fn pubky(&self) -> &PublicKey { + &self.pubky + } + + /// A unique ID for this [AuthToken], which is a concatenation of + /// [AuthToken::pubky] and [AuthToken::timestamp]. + /// + /// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::pubky]. + fn id(version: u8, bytes: &[u8]) -> Box<[u8]> { + match version { + 0 => bytes[75..115].into(), + _ => unreachable!(), + } + } + + fn signable(version: u8, bytes: &[u8]) -> &[u8] { + match version { + 0 => bytes[65..].into(), + _ => unreachable!(), + } + } +} + +#[derive(Debug, Clone, Default)] +/// Keeps track of used AuthToken until they expire. +pub struct AuthVerifier { + seen: Arc>>>, +} + +impl AuthVerifier { + pub fn verify(&self, bytes: &[u8]) -> Result { self.gc(); - if bytes.len() != 96 { - return Err(AuthnSignatureError::InvalidLength(bytes.len())); + let token = AuthToken::verify(bytes)?; + + let mut seen = self.seen.lock().unwrap(); + + let id = AuthToken::id(token.version, bytes); + + match seen.binary_search_by(|element| element.cmp(&id)) { + Ok(_) => Err(Error::AlreadyUsed), + Err(index) => { + seen.insert(index, id); + Ok(token) + } } - - let signature_bytes: SignatureBytes = bytes[0..64] - .try_into() - .expect("validate token length on instantiating"); - let signature = Signature::from(signature_bytes); - - let token_hash: [u8; 32] = bytes[64..].try_into().expect("should not be reachable"); - - let now = Timestamp::now().into_inner(); - let past = now - TIME_INTERVAL; - let future = now + TIME_INTERVAL; - - let result = verify_at(now, self, &signature, signer, &token_hash); - - match result { - Ok(_) => return Ok(()), - Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), - _ => {} - } - - let result = verify_at(past, self, &signature, signer, &token_hash); - - match result { - Ok(_) => return Ok(()), - Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), - _ => {} - } - - verify_at(future, self, &signature, signer, &token_hash) } // === Private Methods === @@ -108,7 +157,7 @@ impl AuthnVerifier { fn gc(&self) { let threshold = ((Timestamp::now().into_inner() / TIME_INTERVAL) - 2).to_be_bytes(); - let mut inner = self.inner.lock().unwrap(); + let mut inner = self.seen.lock().unwrap(); match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) { Ok(index) | Err(index) => { @@ -118,103 +167,113 @@ impl AuthnVerifier { } } -fn verify_at( - time: u64, - verifier: &AuthnVerifier, - signature: &Signature, - signer: &PublicKey, - token_hash: &[u8; 32], -) -> Result<(), AuthnSignatureError> { - let time_step = time / TIME_INTERVAL; - let time_step_bytes = time_step.to_be_bytes(); - - let result = signer.verify( - &signable(&time_step_bytes, signer, &verifier.audience, token_hash), - signature, - ); - - if result.is_ok() { - let mut inner = verifier.inner.lock().unwrap(); - - let mut candidate = [0_u8; 40]; - candidate[..8].copy_from_slice(&time_step_bytes); - candidate[8..].copy_from_slice(token_hash); - - match inner.binary_search_by(|element| element.cmp(&candidate)) { - Ok(index) | Err(index) => { - inner.insert(index, candidate); - } - }; - - return Ok(()); - } - - Err(AuthnSignatureError::InvalidSignature) -} - -fn signable( - time_step_bytes: &[u8; 8], - signer: &PublicKey, - audience: &PublicKey, - token_hash: &[u8; 32], -) -> [u8; 115] { - let mut arr = [0; 115]; - - arr[..11].copy_from_slice(crate::namespaces::PUBKY_AUTHN); - arr[11..19].copy_from_slice(time_step_bytes); - arr[19..51].copy_from_slice(signer.as_bytes()); - arr[51..83].copy_from_slice(audience.as_bytes()); - arr[83..].copy_from_slice(token_hash); - - arr -} - -#[derive(thiserror::Error, Debug)] -pub enum AuthnSignatureError { - #[error("AuthnSignature should be 96 bytes long, got {0} bytes instead")] - InvalidLength(usize), - - #[error("Invalid signature")] +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum Error { + #[error("Unknown version")] + UnknownVersion, + #[error("AuthToken has a timestamp that is more than 45 seconds in the future")] + TooFarInTheFuture, + #[error("AuthToken has a timestamp that is more than 45 seconds in the past")] + Expired, + #[error("Invalid Signature")] InvalidSignature, - - #[error("Authn signature already used")] + #[error(transparent)] + Postcard(#[from] postcard::Error), + #[error("AuthToken already used")] AlreadyUsed, } #[cfg(test)] mod tests { - use crate::crypto::Keypair; + use crate::{ + auth::TIMESTAMP_WINDOW, capabilities::Capability, crypto::Keypair, timestamp::Timestamp, + }; - use super::{AuthnSignature, AuthnVerifier}; + use super::*; + + #[test] + fn v0_id_signable() { + let signer = Keypair::random(); + let capabilities = vec![Capability::root()]; + + let token = AuthToken::sign(&signer, capabilities.clone()); + + let serialized = &token.serialize(); + + let mut id = vec![]; + id.extend_from_slice(&token.timestamp.to_bytes()); + id.extend_from_slice(signer.public_key().as_bytes()); + + assert_eq!(AuthToken::id(token.version, serialized), id.into()); + + assert_eq!( + AuthToken::signable(token.version, serialized), + &serialized[65..] + ) + } #[test] fn sign_verify() { - let keypair = Keypair::random(); - let signer = keypair.public_key(); - let audience = Keypair::random().public_key(); + let signer = Keypair::random(); + let capabilities = vec![Capability::root()]; - let verifier = AuthnVerifier::new(audience.clone()); + let verifier = AuthVerifier::default(); - let authn_signature = AuthnSignature::generate(&keypair, &audience); + let token = AuthToken::sign(&signer, capabilities.clone()); - verifier - .verify(authn_signature.as_bytes(), &signer) - .unwrap(); + let serialized = &token.serialize(); - { - // Invalid signable - let mut invalid = authn_signature.as_bytes().to_vec(); - invalid[64..].copy_from_slice(&[0; 32]); + verifier.verify(serialized).unwrap(); - assert!(!verifier.verify(&invalid, &signer).is_ok()) - } + assert_eq!(token.capabilities, capabilities.into()); + } - { - // Invalid signer - let mut invalid = authn_signature.as_bytes().to_vec(); - invalid[0..32].copy_from_slice(&[0; 32]); + #[test] + fn expired() { + let signer = Keypair::random(); + let capabilities = Capabilities(vec![Capability::root()]); - assert!(!verifier.verify(&invalid, &signer).is_ok()) - } + let verifier = AuthVerifier::default(); + + let timestamp = (&Timestamp::now()) - (TIMESTAMP_WINDOW as u64); + + let mut signable = vec![]; + signable.extend_from_slice(signer.public_key().as_bytes()); + signable.extend_from_slice(&postcard::to_allocvec(&capabilities).unwrap()); + + let signature = signer.sign(&signable); + + let token = AuthToken { + signature, + namespace: *PUBKY_AUTH, + version: 0, + timestamp, + pubky: signer.public_key(), + capabilities, + }; + + let serialized = token.serialize(); + + let result = verifier.verify(&serialized); + + assert_eq!(result, Err(Error::Expired)); + } + + #[test] + fn already_used() { + let signer = Keypair::random(); + let capabilities = vec![Capability::root()]; + + let verifier = AuthVerifier::default(); + + let token = AuthToken::sign(&signer, capabilities.clone()); + + let serialized = &token.serialize(); + + verifier.verify(serialized).unwrap(); + + assert_eq!(token.capabilities, capabilities.into()); + + assert_eq!(verifier.verify(serialized), Err(Error::AlreadyUsed)); } } diff --git a/pubky-common/src/capabilities.rs b/pubky-common/src/capabilities.rs new file mode 100644 index 0000000..7929860 --- /dev/null +++ b/pubky-common/src/capabilities.rs @@ -0,0 +1,237 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Capability { + pub scope: String, + pub actions: Vec, +} + +impl Capability { + /// Create a root [Capability] at the `/` path with all the available [PubkyAbility] + pub fn root() -> Self { + Capability { + scope: "/".to_string(), + actions: vec![Action::Read, Action::Write], + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Action { + /// Can read the scope at the specified path (GET requests). + Read, + /// Can write to the scope at the specified path (PUT/POST/DELETE requests). + Write, + /// Unknown ability + Unknown(char), +} + +impl From<&Action> for char { + fn from(value: &Action) -> Self { + match value { + Action::Read => 'r', + Action::Write => 'w', + Action::Unknown(char) => char.to_owned(), + } + } +} + +impl TryFrom for Action { + type Error = Error; + + fn try_from(value: char) -> Result { + match value { + 'r' => Ok(Self::Read), + 'w' => Ok(Self::Write), + _ => Err(Error::InvalidAction), + } + } +} + +impl Display for Capability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{}", + self.scope, + self.actions.iter().map(char::from).collect::() + ) + } +} + +impl TryFrom for Capability { + type Error = Error; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl TryFrom<&str> for Capability { + type Error = Error; + + fn try_from(value: &str) -> Result { + if value.matches(':').count() != 1 { + return Err(Error::InvalidFormat); + } + + if !value.starts_with('/') { + return Err(Error::InvalidScope); + } + + let actions_str = value.rsplit(':').next().unwrap_or(""); + + let mut actions = Vec::new(); + + for char in actions_str.chars() { + let ability = Action::try_from(char)?; + + match actions.binary_search_by(|element| char::from(element).cmp(&char)) { + Ok(_) => {} + Err(index) => { + actions.insert(index, ability); + } + } + } + + let scope = value[0..value.len() - actions_str.len() - 1].to_string(); + + Ok(Capability { scope, actions }) + } +} + +impl Serialize for Capability { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let string = self.to_string(); + + string.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Capability { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string: String = Deserialize::deserialize(deserializer)?; + + string.try_into().map_err(serde::de::Error::custom) + } +} + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum Error { + #[error("Capability: Invalid scope: does not start with `/`")] + InvalidScope, + #[error("Capability: Invalid format should be :")] + InvalidFormat, + #[error("Capability: Invalid Action")] + InvalidAction, + #[error("Capabilities: Invalid capabilities format")] + InvalidCapabilities, +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +/// A wrapper around `Vec` to enable serialization without +/// a varint. Useful when [Capabilities] are at the end of a struct. +pub struct Capabilities(pub Vec); + +impl Capabilities { + pub fn contains(&self, capability: &Capability) -> bool { + self.0.contains(capability) + } +} + +impl From> for Capabilities { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From for Vec { + fn from(value: Capabilities) -> Self { + value.0 + } +} + +impl TryFrom<&str> for Capabilities { + type Error = Error; + + fn try_from(value: &str) -> Result { + let mut caps = vec![]; + + for s in value.split(',') { + if let Ok(cap) = Capability::try_from(s) { + caps.push(cap); + }; + } + + Ok(Capabilities(caps)) + } +} + +impl Display for Capabilities { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = self + .0 + .iter() + .map(|c| c.to_string()) + .collect::>() + .join(","); + + write!(f, "{}", string) + } +} + +impl Serialize for Capabilities { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Capabilities { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string: String = Deserialize::deserialize(deserializer)?; + + let mut caps = vec![]; + + for s in string.split(',') { + if let Ok(cap) = Capability::try_from(s) { + caps.push(cap); + }; + } + + Ok(Capabilities(caps)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pubky_caps() { + let cap = Capability { + scope: "/pub/pubky.app/".to_string(), + actions: vec![Action::Read, Action::Write], + }; + + // Read and write withing directory `/pub/pubky.app/`. + let expected_string = "/pub/pubky.app/:rw"; + + assert_eq!(cap.to_string(), expected_string); + + assert_eq!(Capability::try_from(expected_string), Ok(cap)) + } +} diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index 2f8131c..a7adea5 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -1,3 +1,7 @@ +use crypto_secretbox::{ + aead::{Aead, AeadCore, KeyInit, OsRng}, + XSalsa20Poly1305, +}; use rand::prelude::Rng; pub use pkarr::{Keypair, PublicKey}; @@ -8,6 +12,8 @@ pub type Hash = blake3::Hash; pub use blake3::hash; +pub use blake3::Hasher; + pub fn random_hash() -> Hash { let mut rng = rand::thread_rng(); Hash::from_bytes(rng.gen()) @@ -23,3 +29,43 @@ pub fn random_bytes() -> [u8; N] { } arr } + +pub fn encrypt(plain_text: &[u8], encryption_key: &[u8; 32]) -> Result, Error> { + let cipher = XSalsa20Poly1305::new(encryption_key.into()); + let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng); // unique per message + let ciphertext = cipher.encrypt(&nonce, plain_text)?; + + let mut out: Vec = Vec::with_capacity(nonce.len() + ciphertext.len()); + out.extend_from_slice(nonce.as_slice()); + out.extend_from_slice(&ciphertext); + + Ok(out) +} + +pub fn decrypt(bytes: &[u8], encryption_key: &[u8; 32]) -> Result, Error> { + let cipher = XSalsa20Poly1305::new(encryption_key.into()); + + Ok(cipher.decrypt(bytes[..24].into(), &bytes[24..])?) +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + SecretBox(#[from] crypto_secretbox::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_decrypt() { + let plain_text = "Plain text!"; + let encryption_key = [0; 32]; + + let encrypted = encrypt(plain_text.as_bytes(), &encryption_key).unwrap(); + let decrypted = decrypt(&encrypted, &encryption_key).unwrap(); + + assert_eq!(decrypted, plain_text.as_bytes()) + } +} diff --git a/pubky-common/src/lib.rs b/pubky-common/src/lib.rs index cedc227..cfb56f2 100644 --- a/pubky-common/src/lib.rs +++ b/pubky-common/src/lib.rs @@ -1,5 +1,7 @@ pub mod auth; +pub mod capabilities; pub mod crypto; pub mod namespaces; +pub mod recovery_file; pub mod session; pub mod timestamp; diff --git a/pubky-common/src/namespaces.rs b/pubky-common/src/namespaces.rs index 6c951dd..6aa37cd 100644 --- a/pubky-common/src/namespaces.rs +++ b/pubky-common/src/namespaces.rs @@ -1 +1 @@ -pub const PUBKY_AUTHN: &[u8; 11] = b"PUBKY:AUTHN"; +pub const PUBKY_AUTH: &[u8; 10] = b"PUBKY:AUTH"; diff --git a/pubky-common/src/recovery_file.rs b/pubky-common/src/recovery_file.rs new file mode 100644 index 0000000..0a2f9b4 --- /dev/null +++ b/pubky-common/src/recovery_file.rs @@ -0,0 +1,102 @@ +use argon2::Argon2; +use pkarr::Keypair; + +use crate::crypto::{decrypt, encrypt}; + +static SPEC_NAME: &str = "recovery"; +static SPEC_LINE: &str = "pubky.org/recovery"; + +pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { + let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; + + let newline_index = recovery_file + .iter() + .position(|&r| r == 10) + .ok_or(()) + .map_err(|_| Error::RecoveryFileMissingSpecLine)?; + + let spec_line = &recovery_file[..newline_index]; + + if !(spec_line.starts_with(SPEC_LINE.as_bytes()) + || spec_line.starts_with(b"pkarr.org/recovery")) + { + return Err(Error::RecoveryFileVersionNotSupported); + } + + let encrypted = &recovery_file[newline_index + 1..]; + + if encrypted.is_empty() { + return Err(Error::RecoverFileMissingEncryptedSecretKey); + }; + + let decrypted = decrypt(encrypted, &encryption_key)?; + let length = decrypted.len(); + let secret_key: [u8; 32] = decrypted + .try_into() + .map_err(|_| Error::RecoverFileInvalidSecretKeyLength(length))?; + + Ok(Keypair::from_secret_key(&secret_key)) +} + +pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result, Error> { + let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; + let secret_key = keypair.secret_key(); + + let encrypted_secret_key = encrypt(&secret_key, &encryption_key)?; + + let mut out = Vec::with_capacity(SPEC_LINE.len() + 1 + encrypted_secret_key.len()); + + out.extend_from_slice(SPEC_LINE.as_bytes()); + out.extend_from_slice(b"\n"); + out.extend_from_slice(&encrypted_secret_key); + + Ok(out) +} + +fn recovery_file_encryption_key_from_passphrase(passphrase: &str) -> Result<[u8; 32], Error> { + let argon2id = Argon2::default(); + + let mut out = [0; 32]; + + argon2id.hash_password_into(passphrase.as_bytes(), SPEC_NAME.as_bytes(), &mut out)?; + + Ok(out) +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + // === Recovery file == + #[error("Recovery file should start with a spec line, followed by a new line character")] + RecoveryFileMissingSpecLine, + + #[error("Recovery file should start with a spec line, followed by a new line character")] + RecoveryFileVersionNotSupported, + + #[error("Recovery file should contain an encrypted secret key after the new line character")] + RecoverFileMissingEncryptedSecretKey, + + #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")] + RecoverFileInvalidSecretKeyLength(usize), + + #[error(transparent)] + Argon(#[from] argon2::Error), + + #[error(transparent)] + Crypto(#[from] crate::crypto::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_decrypt_recovery_file() { + let passphrase = "very secure password"; + let keypair = Keypair::random(); + + let recovery_file = create_recovery_file(&keypair, passphrase).unwrap(); + let recovered = decrypt_recovery_file(&recovery_file, passphrase).unwrap(); + + assert_eq!(recovered.public_key(), keypair.public_key()); + } +} diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 9ef3c9d..5ce64d0 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -1,31 +1,48 @@ +use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; extern crate alloc; use alloc::vec::Vec; -use crate::timestamp::Timestamp; +use crate::{auth::AuthToken, capabilities::Capability, timestamp::Timestamp}; // TODO: add IP address? // TODO: use https://crates.io/crates/user-agent-parser to parse the session // and get more informations from the user-agent. -#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Session { - pub version: usize, - pub created_at: u64, + version: usize, + pubky: PublicKey, + created_at: u64, /// User specified name, defaults to the user-agent. - pub name: String, - pub user_agent: String, + name: String, + user_agent: String, + capabilities: Vec, } impl Session { - pub fn new() -> Self { + pub fn new(token: &AuthToken, user_agent: Option) -> Self { Self { + version: 0, + pubky: token.pubky().to_owned(), created_at: Timestamp::now().into_inner(), - ..Default::default() + capabilities: token.capabilities().to_vec(), + user_agent: user_agent.as_deref().unwrap_or("").to_string(), + name: user_agent.as_deref().unwrap_or("").to_string(), } } + // === Getters === + + pub fn pubky(&self) -> &PublicKey { + &self.pubky + } + + pub fn capabilities(&self) -> &Vec { + &self.capabilities + } + // === Setters === pub fn set_user_agent(&mut self, user_agent: String) -> &mut Self { @@ -38,6 +55,12 @@ impl Session { self } + pub fn set_capabilities(&mut self, capabilities: Vec) -> &mut Self { + self.capabilities = capabilities; + + self + } + // === Public Methods === pub fn serialize(&self) -> Vec { @@ -51,6 +74,8 @@ impl Session { Ok(from_bytes(bytes)?) } + + // TODO: add `can_read()`, `can_write()` and `is_root()` methods } pub type Result = core::result::Result; @@ -65,17 +90,34 @@ pub enum Error { #[cfg(test)] mod tests { + use crate::crypto::Keypair; + use super::*; #[test] fn serialize() { - let mut session = Session::default(); + let keypair = Keypair::from_secret_key(&[0; 32]); + let pubky = keypair.public_key(); - session.user_agent = "foo".to_string(); + let session = Session { + user_agent: "foo".to_string(), + capabilities: vec![Capability::root()], + created_at: 0, + pubky, + version: 0, + name: "".to_string(), + }; let serialized = session.serialize(); - assert_eq!(serialized, [0, 0, 0, 3, 102, 111, 111,]); + assert_eq!( + serialized, + [ + 0, 59, 106, 39, 188, 206, 182, 164, 45, 98, 163, 168, 208, 42, 111, 13, 115, 101, + 50, 21, 119, 29, 226, 67, 166, 58, 192, 72, 161, 139, 89, 218, 41, 0, 0, 3, 102, + 111, 111, 1, 4, 47, 58, 114, 119 + ] + ); let deseiralized = Session::deserialize(&serialized).unwrap(); diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index f850661..848f894 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -1,7 +1,7 @@ //! Monotonic unix timestamp in microseconds +use serde::{Deserialize, Serialize}; use std::fmt::Display; -use std::time::SystemTime; use std::{ ops::{Add, Sub}, sync::Mutex, @@ -10,6 +10,9 @@ use std::{ use once_cell::sync::Lazy; use rand::Rng; +#[cfg(not(target_arch = "wasm32"))] +use std::time::SystemTime; + /// ~4% chance of none of 10 clocks have matching id. const CLOCK_MASK: u64 = (1 << 8) - 1; const TIME_MASK: u64 = !0 >> 8; @@ -72,8 +75,8 @@ impl Timestamp { self.0.to_be_bytes() } - pub fn difference(&self, rhs: &Timestamp) -> u64 { - self.0.abs_diff(rhs.0) + pub fn difference(&self, rhs: &Timestamp) -> i64 { + (self.0 as i64) - (rhs.0 as i64) } pub fn into_inner(&self) -> u64 { @@ -81,6 +84,12 @@ impl Timestamp { } } +impl Default for Timestamp { + fn default() -> Self { + Timestamp::now() + } +} + impl Display for Timestamp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let bytes: [u8; 8] = self.into(); @@ -153,6 +162,26 @@ impl Sub for &Timestamp { } } +impl Serialize for Timestamp { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bytes = self.to_bytes(); + bytes.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes: [u8; 8] = Deserialize::deserialize(deserializer)?; + Ok(Timestamp(u64::from_be_bytes(bytes))) + } +} + #[cfg(not(target_arch = "wasm32"))] /// Return the number of microseconds since [SystemTime::UNIX_EPOCH] fn system_time() -> u64 { @@ -162,6 +191,15 @@ fn system_time() -> u64 { .as_micros() as u64 } +#[cfg(target_arch = "wasm32")] +/// Return the number of microseconds since [SystemTime::UNIX_EPOCH] +pub fn system_time() -> u64 { + // Won't be an issue for more than 5000 years! + (js_sys::Date::now() as u64 ) + // Turn miliseconds to microseconds + * 1000 +} + #[derive(thiserror::Error, Debug)] pub enum TimestampError { #[error("Invalid bytes length, Timestamp should be encoded as 8 bytes, got {0}")] @@ -226,4 +264,17 @@ mod tests { assert_eq!(decoded, timestamp) } + + #[test] + fn serde() { + let timestamp = Timestamp::now(); + + let serialized = postcard::to_allocvec(×tamp).unwrap(); + + assert_eq!(serialized, timestamp.to_bytes()); + + let deserialized: Timestamp = postcard::from_bytes(&serialized).unwrap(); + + assert_eq!(deserialized, timestamp); + } } diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 33e18ab..c8abfd5 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -5,17 +5,22 @@ edition = "2021" [dependencies] anyhow = "1.0.82" -axum = "0.7.5" -axum-extra = { version = "0.9.3", features = ["typed-header"] } +axum = { version = "0.7.5", features = ["macros"] } +axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] } base32 = "0.5.1" -bytes = "1.6.1" +bytes = "^1.7.1" +clap = { version = "4.5.11", features = ["derive"] } dirs-next = "2.0.0" +flume = "0.11.0" +futures-util = "0.3.30" heed = "0.20.3" -pkarr = { version = "2.1.0", features = ["async"] } +hex = "0.4.3" +pkarr = { workspace = true } postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } -serde = { version = "1.0.204", features = ["derive"] } +serde = { workspace = true } tokio = { version = "1.37.0", features = ["full"] } +toml = "0.8.19" tower-cookies = "0.10.0" tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" diff --git a/pubky-homeserver/README.md b/pubky-homeserver/README.md new file mode 100644 index 0000000..d1799a2 --- /dev/null +++ b/pubky-homeserver/README.md @@ -0,0 +1,23 @@ +# Pubky Homeserver + +## Usage + +Use `cargo run` + +```bash +cargo run -- --config=./src/config.toml +``` + +Or Build first then run from target. + +Build + +```bash +cargo build --release +``` + +Run with an optional config file + +```bash +../target/release/pubky-homeserver --config=./src/config.toml +``` diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 7c9fcfe..55f015c 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -1,9 +1,14 @@ //! Configuration for the server -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use pkarr::Keypair; -// use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, path::PathBuf}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::{ + fmt::Debug, + path::{Path, PathBuf}, + time::Duration, +}; +use tracing::info; use pubky_common::timestamp::Timestamp; @@ -11,13 +16,9 @@ const DEFAULT_HOMESERVER_PORT: u16 = 6287; const DEFAULT_STORAGE_DIR: &str = "pubky"; /// Server configuration -/// -/// The config is usually loaded from a file with [`Self::load`]. -#[derive( - // Serialize, Deserialize, - Clone, -)] +#[derive(Serialize, Deserialize, Clone)] pub struct Config { + testnet: bool, port: Option, bootstrap: Option>, domain: String, @@ -25,28 +26,66 @@ pub struct Config { /// /// Defaults to a directory in the OS data directory storage: Option, - keypair: Keypair, + #[serde(deserialize_with = "secret_key_deserialize")] + secret_key: Option<[u8; 32]>, + + dht_request_timeout: Option, } impl Config { - // /// Load the config from a file. - // pub async fn load(path: impl AsRef) -> Result { - // let s = tokio::fs::read_to_string(path.as_ref()) - // .await - // .with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?; - // let config: Config = toml::from_str(&s)?; - // Ok(config) - // } + /// Load the config from a file. + pub async fn load(path: impl AsRef) -> Result { + let s = tokio::fs::read_to_string(path.as_ref()) + .await + .with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?; + + let config: Config = toml::from_str(&s)?; + + if config.testnet { + let testnet_config = Config::testnet(); + + return Ok(Config { + bootstrap: testnet_config.bootstrap, + ..config + }); + } + + Ok(config) + } + + /// Testnet configurations + pub fn testnet() -> Self { + let testnet = pkarr::mainline::Testnet::new(10); + info!(?testnet.bootstrap, "Testnet bootstrap nodes"); + + let bootstrap = Some(testnet.bootstrap.to_owned()); + let storage = Some( + std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join(DEFAULT_STORAGE_DIR), + ); + + Self { + bootstrap, + storage, + port: Some(15411), + dht_request_timeout: Some(Duration::from_millis(10)), + ..Default::default() + } + } /// Test configurations pub fn test(testnet: &pkarr::mainline::Testnet) -> Self { + let bootstrap = Some(testnet.bootstrap.to_owned()); + let storage = Some( + std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join(DEFAULT_STORAGE_DIR), + ); + Self { - bootstrap: Some(testnet.bootstrap.to_owned()), - storage: Some( - std::env::temp_dir() - .join(Timestamp::now().to_string()) - .join(DEFAULT_STORAGE_DIR), - ), + bootstrap, + storage, ..Default::default() } } @@ -77,26 +116,55 @@ impl Config { Ok(dir.join("homeserver")) } - pub fn keypair(&self) -> &Keypair { - &self.keypair + pub fn keypair(&self) -> Keypair { + Keypair::from_secret_key(&self.secret_key.unwrap_or_default()) + } + + pub(crate) fn dht_request_timeout(&self) -> Option { + self.dht_request_timeout } } impl Default for Config { fn default() -> Self { Self { + testnet: false, port: Some(0), bootstrap: None, domain: "localhost".to_string(), storage: None, - keypair: Keypair::random(), + secret_key: None, + dht_request_timeout: None, } } } +fn secret_key_deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt: Option = Option::deserialize(deserializer)?; + + match opt { + Some(s) => { + let bytes = hex::decode(s).map_err(serde::de::Error::custom)?; + + if bytes.len() != 32 { + return Err(serde::de::Error::custom("Expected a 32-byte array")); + } + + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + Ok(Some(arr)) + } + None => Ok(None), + } +} + impl Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_map() + .entry(&"testnet", &self.testnet) .entry(&"port", &self.port()) .entry(&"storage", &self.storage()) .entry(&"public_key", &self.keypair().public_key()) diff --git a/pubky-homeserver/src/config.toml b/pubky-homeserver/src/config.toml new file mode 100644 index 0000000..2012efc --- /dev/null +++ b/pubky-homeserver/src/config.toml @@ -0,0 +1,10 @@ +# Use testnet network (local DHT) for testing. +testnet = false +# Secret key (in hex) to generate the Homeserver's Keypair +secret_key = "0000000000000000000000000000000000000000000000000000000000000000" +# Domain to be published in Pkarr records for this server to be accessible by. +domain = "localhost" +# Port for the Homeserver to listen on. +port = 6287 +# Storage directory Defaults to +# storage = "" diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 2f8d591..4adc73d 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -1,16 +1,20 @@ use std::fs; + use std::path::Path; -use heed::{types::Str, Database, Env, EnvOpenOptions, RwTxn}; +use heed::{Env, EnvOpenOptions}; mod migrations; pub mod tables; -use migrations::TABLES_COUNT; +use tables::{Tables, TABLES_COUNT}; + +pub const MAX_LIST_LIMIT: u16 = 100; #[derive(Debug, Clone)] pub struct DB { pub(crate) env: Env, + pub(crate) tables: Tables, } impl DB { @@ -19,21 +23,51 @@ impl DB { let env = unsafe { EnvOpenOptions::new().max_dbs(TABLES_COUNT).open(storage) }?; - let db = DB { env }; + let tables = migrations::run(&env)?; - db.run_migrations(); + let db = DB { env, tables }; Ok(db) } +} - fn run_migrations(&self) -> anyhow::Result<()> { - let mut wtxn = self.env.write_txn()?; +#[cfg(test)] +mod tests { + use bytes::Bytes; + use pkarr::Keypair; + use pubky_common::timestamp::Timestamp; - migrations::create_users_table(&self.env, &mut wtxn); - migrations::create_sessions_table(&self.env, &mut wtxn); + use super::DB; - wtxn.commit()?; + #[tokio::test] + async fn entries() { + let storage = std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join("pubky"); - Ok(()) + let db = DB::open(&storage).unwrap(); + + let keypair = Keypair::random(); + let path = "/pub/foo.txt"; + + let (tx, rx) = flume::bounded::(0); + + let mut cloned = db.clone(); + let cloned_keypair = keypair.clone(); + + let done = tokio::task::spawn_blocking(move || { + cloned + .put_entry(&cloned_keypair.public_key(), path, rx) + .unwrap(); + }); + + tx.send(vec![1, 2, 3, 4, 5].into()).unwrap(); + drop(tx); + + done.await.unwrap(); + + let blob = db.get_blob(&keypair.public_key(), path).unwrap().unwrap(); + + assert_eq!(blob, Bytes::from(vec![1, 2, 3, 4, 5])); } } diff --git a/pubky-homeserver/src/database/migrations.rs b/pubky-homeserver/src/database/migrations.rs index 93c7631..eb5a5f8 100644 --- a/pubky-homeserver/src/database/migrations.rs +++ b/pubky-homeserver/src/database/migrations.rs @@ -1,19 +1,17 @@ -use heed::{types::Str, Database, Env, RwTxn}; +use heed::Env; -use super::tables; +mod m0; -pub const TABLES_COUNT: u32 = 2; +use super::tables::Tables; -pub fn create_users_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { - let _: tables::users::UsersTable = - env.create_database(wtxn, Some(tables::users::USERS_TABLE))?; +pub fn run(env: &Env) -> anyhow::Result { + let mut wtxn = env.write_txn()?; - Ok(()) -} - -pub fn create_sessions_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { - let _: tables::sessions::SessionsTable = - env.create_database(wtxn, Some(tables::sessions::SESSIONS_TABLE))?; - - Ok(()) + m0::run(env, &mut wtxn)?; + + let tables = Tables::new(env, &mut wtxn)?; + + wtxn.commit()?; + + Ok(tables) } diff --git a/pubky-homeserver/src/database/migrations/m0.rs b/pubky-homeserver/src/database/migrations/m0.rs new file mode 100644 index 0000000..11c0e1a --- /dev/null +++ b/pubky-homeserver/src/database/migrations/m0.rs @@ -0,0 +1,17 @@ +use heed::{Env, RwTxn}; + +use crate::database::tables::{blobs, entries, events, sessions, users}; + +pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { + let _: users::UsersTable = env.create_database(wtxn, Some(users::USERS_TABLE))?; + + let _: sessions::SessionsTable = env.create_database(wtxn, Some(sessions::SESSIONS_TABLE))?; + + let _: blobs::BlobsTable = env.create_database(wtxn, Some(blobs::BLOBS_TABLE))?; + + let _: entries::EntriesTable = env.create_database(wtxn, Some(entries::ENTRIES_TABLE))?; + + let _: events::EventsTable = env.create_database(wtxn, Some(events::EVENTS_TABLE))?; + + Ok(()) +} diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index b6e3efc..e879bd0 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -1,2 +1,49 @@ +pub mod blobs; +pub mod entries; +pub mod events; pub mod sessions; pub mod users; + +use heed::{Env, RwTxn}; + +use blobs::{BlobsTable, BLOBS_TABLE}; +use entries::{EntriesTable, ENTRIES_TABLE}; + +use self::{ + events::{EventsTable, EVENTS_TABLE}, + sessions::{SessionsTable, SESSIONS_TABLE}, + users::{UsersTable, USERS_TABLE}, +}; + +pub const TABLES_COUNT: u32 = 5; + +#[derive(Debug, Clone)] +pub struct Tables { + pub users: UsersTable, + pub sessions: SessionsTable, + pub blobs: BlobsTable, + pub entries: EntriesTable, + pub events: EventsTable, +} + +impl Tables { + pub fn new(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result { + Ok(Self { + users: env + .open_database(wtxn, Some(USERS_TABLE))? + .expect("Users table already created"), + sessions: env + .open_database(wtxn, Some(SESSIONS_TABLE))? + .expect("Sessions table already created"), + blobs: env + .open_database(wtxn, Some(BLOBS_TABLE))? + .expect("Blobs table already created"), + entries: env + .open_database(wtxn, Some(ENTRIES_TABLE))? + .expect("Entries table already created"), + events: env + .open_database(wtxn, Some(EVENTS_TABLE))? + .expect("Events table already created"), + }) + } +} diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs new file mode 100644 index 0000000..25f57c0 --- /dev/null +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -0,0 +1,38 @@ +use heed::{types::Bytes, Database}; +use pkarr::PublicKey; + +use crate::database::DB; + +use super::entries::Entry; + +/// hash of the blob => bytes. +pub type BlobsTable = Database; + +pub const BLOBS_TABLE: &str = "blobs"; + +impl DB { + pub fn get_blob( + &self, + public_key: &PublicKey, + path: &str, + ) -> anyhow::Result> { + let rtxn = self.env.read_txn()?; + + let key = format!("{public_key}/{path}"); + + let result = if let Some(bytes) = self.tables.entries.get(&rtxn, &key)? { + let entry = Entry::deserialize(bytes)?; + + self.tables + .blobs + .get(&rtxn, entry.content_hash())? + .map(|blob| bytes::Bytes::from(blob.to_vec())) + } else { + None + }; + + rtxn.commit()?; + + Ok(result) + } +} diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs new file mode 100644 index 0000000..e41a5df --- /dev/null +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -0,0 +1,274 @@ +use pkarr::PublicKey; +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use heed::{ + types::{Bytes, Str}, + Database, RoTxn, +}; + +use pubky_common::{ + crypto::{Hash, Hasher}, + timestamp::Timestamp, +}; + +use crate::database::{DB, MAX_LIST_LIMIT}; + +use super::events::Event; + +/// full_path(pubky/*path) => Entry. +pub type EntriesTable = Database; + +pub const ENTRIES_TABLE: &str = "entries"; + +impl DB { + pub fn put_entry( + &mut self, + public_key: &PublicKey, + path: &str, + rx: flume::Receiver, + ) -> anyhow::Result<()> { + let mut wtxn = self.env.write_txn()?; + + let mut hasher = Hasher::new(); + let mut bytes = vec![]; + let mut length = 0; + + while let Ok(chunk) = rx.recv() { + hasher.update(&chunk); + bytes.extend_from_slice(&chunk); + length += chunk.len(); + } + + let hash = hasher.finalize(); + + self.tables.blobs.put(&mut wtxn, hash.as_bytes(), &bytes)?; + + let mut entry = Entry::new(); + + entry.set_content_hash(hash); + entry.set_content_length(length); + + let key = format!("{public_key}/{path}"); + + self.tables + .entries + .put(&mut wtxn, &key, &entry.serialize())?; + + if path.starts_with("pub/") { + let url = format!("pubky://{key}"); + let event = Event::put(&url); + let value = event.serialize(); + + let key = entry.timestamp.to_string(); + + self.tables.events.put(&mut wtxn, &key, &value)?; + + // TODO: delete older events. + // TODO: move to events.rs + } + + wtxn.commit()?; + + Ok(()) + } + + pub fn delete_entry(&mut self, public_key: &PublicKey, path: &str) -> anyhow::Result { + let mut wtxn = self.env.write_txn()?; + + let key = format!("{public_key}/{path}"); + + let deleted = if let Some(bytes) = self.tables.entries.get(&wtxn, &key)? { + let entry = Entry::deserialize(bytes)?; + + // TODO: reference counting of blobs + let deleted_blobs = self.tables.blobs.delete(&mut wtxn, entry.content_hash())?; + + let deleted_entry = self.tables.entries.delete(&mut wtxn, &key)?; + + // create DELETE event + if path.starts_with("pub/") { + let url = format!("pubky://{key}"); + + let event = Event::delete(&url); + let value = event.serialize(); + + let key = Timestamp::now().to_string(); + + self.tables.events.put(&mut wtxn, &key, &value)?; + + // TODO: delete older events. + // TODO: move to events.rs + } + + deleted_entry & deleted_blobs + } else { + false + }; + + wtxn.commit()?; + + Ok(deleted) + } + + pub fn contains_directory(&self, txn: &RoTxn, path: &str) -> anyhow::Result { + Ok(self.tables.entries.get_greater_than(txn, path)?.is_some()) + } + + /// Return a list of pubky urls. + /// + /// - limit defaults to and capped by [MAX_LIST_LIMIT] + pub fn list( + &self, + txn: &RoTxn, + path: &str, + reverse: bool, + limit: Option, + cursor: Option, + shallow: bool, + ) -> anyhow::Result> { + // Vector to store results + let mut results = Vec::new(); + + let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); + + // TODO: make this more performant than split and allocations? + + let mut threshold = cursor + .map(|cursor| { + // Removing leading forward slashes + let mut file_or_directory = cursor.trim_start_matches('/'); + + if cursor.starts_with("pubky://") { + file_or_directory = cursor.split(path).last().expect("should not be reachable") + }; + + next_threshold( + path, + file_or_directory, + file_or_directory.ends_with('/'), + reverse, + shallow, + ) + }) + .unwrap_or(next_threshold(path, "", false, reverse, shallow)); + + for _ in 0..limit { + if let Some((key, _)) = if reverse { + self.tables.entries.get_lower_than(txn, &threshold)? + } else { + self.tables.entries.get_greater_than(txn, &threshold)? + } { + if !key.starts_with(path) { + break; + } + + if shallow { + let mut split = key[path.len()..].split('/'); + let file_or_directory = split.next().expect("should not be reachable"); + + let is_directory = split.next().is_some(); + + threshold = + next_threshold(path, file_or_directory, is_directory, reverse, shallow); + + results.push(format!( + "pubky://{path}{file_or_directory}{}", + if is_directory { "/" } else { "" } + )); + } else { + threshold = key.to_string(); + results.push(format!("pubky://{}", key)) + } + }; + } + + Ok(results) + } +} + +/// Calculate the next threshold +#[instrument] +fn next_threshold( + path: &str, + file_or_directory: &str, + is_directory: bool, + reverse: bool, + shallow: bool, +) -> String { + format!( + "{path}{file_or_directory}{}", + if file_or_directory.is_empty() { + // No file_or_directory, early return + if reverse { + // `path/to/dir/\x7f` to catch all paths than `path/to/dir/` + "\x7f" + } else { + "" + } + } else if shallow & is_directory { + if reverse { + // threshold = `path/to/dir\x2e`, since `\x2e` is lower than `/` + "\x2e" + } else { + //threshold = `path/to/dir\x7f`, since `\x7f` is greater than `/` + "\x7f" + } + } else { + "" + } + ) +} + +#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Entry { + /// Encoding version + version: usize, + /// Modified at + timestamp: Timestamp, + content_hash: [u8; 32], + content_length: usize, + content_type: String, + // user_metadata: ? +} + +// TODO: get headers like Etag + +impl Entry { + pub fn new() -> Self { + Default::default() + } + + // === Setters === + + pub fn set_content_hash(&mut self, content_hash: Hash) -> &mut Self { + content_hash.as_bytes().clone_into(&mut self.content_hash); + self + } + + pub fn set_content_length(&mut self, content_length: usize) -> &mut Self { + self.content_length = content_length; + self + } + + // === Getters === + + pub fn content_hash(&self) -> &[u8; 32] { + &self.content_hash + } + + // === Public Method === + + pub fn serialize(&self) -> Vec { + to_allocvec(self).expect("Session::serialize") + } + + pub fn deserialize(bytes: &[u8]) -> core::result::Result { + if bytes[0] > 0 { + panic!("Unknown Entry version"); + } + + from_bytes(bytes) + } +} diff --git a/pubky-homeserver/src/database/tables/events.rs b/pubky-homeserver/src/database/tables/events.rs new file mode 100644 index 0000000..cf82e18 --- /dev/null +++ b/pubky-homeserver/src/database/tables/events.rs @@ -0,0 +1,58 @@ +//! Server events (Put and Delete entries) +//! +//! Useful as a realtime sync with Indexers until +//! we implement more self-authenticated merkle data. + +use heed::{ + types::{Bytes, Str}, + Database, +}; +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; + +/// Event [Timestamp] base32 => Encoded event. +pub type EventsTable = Database; + +pub const EVENTS_TABLE: &str = "events"; + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum Event { + Put(String), + Delete(String), +} + +impl Event { + pub fn put(url: &str) -> Self { + Self::Put(url.to_string()) + } + + pub fn delete(url: &str) -> Self { + Self::Delete(url.to_string()) + } + + pub fn serialize(&self) -> Vec { + to_allocvec(self).expect("Session::serialize") + } + + pub fn deserialize(bytes: &[u8]) -> core::result::Result { + if bytes[0] > 1 { + panic!("Unknown Event version"); + } + + from_bytes(bytes) + } + + pub fn url(&self) -> &str { + match self { + Event::Put(url) => url, + Event::Delete(url) => url, + } + } + + pub fn operation(&self) -> &str { + match self { + Event::Put(_) => "PUT", + Event::Delete(_) => "DEL", + } + } +} diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs index 6e9d8f8..4ecd228 100644 --- a/pubky-homeserver/src/database/tables/sessions.rs +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -1,11 +1,51 @@ -use std::{borrow::Cow, time::SystemTime}; - use heed::{ types::{Bytes, Str}, - BoxedError, BytesDecode, BytesEncode, Database, + Database, }; +use pkarr::PublicKey; +use pubky_common::session::Session; +use tower_cookies::Cookies; + +use crate::database::DB; /// session secret => Session. pub type SessionsTable = Database; pub const SESSIONS_TABLE: &str = "sessions"; + +impl DB { + pub fn get_session( + &mut self, + cookies: Cookies, + public_key: &PublicKey, + ) -> anyhow::Result> { + if let Some(bytes) = self.get_session_bytes(cookies, public_key)? { + return Ok(Some(Session::deserialize(&bytes)?)); + }; + + Ok(None) + } + + pub fn get_session_bytes( + &mut self, + cookies: Cookies, + public_key: &PublicKey, + ) -> anyhow::Result>> { + if let Some(cookie) = cookies.get(&public_key.to_string()) { + let rtxn = self.env.read_txn()?; + + let sessions: SessionsTable = self + .env + .open_database(&rtxn, Some(SESSIONS_TABLE))? + .expect("Session table already created"); + + let session = sessions.get(&rtxn, cookie.value())?.map(|s| s.to_vec()); + + rtxn.commit()?; + + return Ok(session); + }; + + Ok(None) + } +} diff --git a/pubky-homeserver/src/database/tables/users.rs b/pubky-homeserver/src/database/tables/users.rs index 9666637..cf9b44e 100644 --- a/pubky-homeserver/src/database/tables/users.rs +++ b/pubky-homeserver/src/database/tables/users.rs @@ -1,14 +1,12 @@ -use std::{borrow::Cow, time::SystemTime}; +use std::borrow::Cow; use postcard::{from_bytes, to_allocvec}; -use pubky_common::timestamp::Timestamp; use serde::{Deserialize, Serialize}; -use heed::{types::Str, BoxedError, BytesDecode, BytesEncode, Database}; +use heed::{BoxedError, BytesDecode, BytesEncode, Database}; use pkarr::PublicKey; extern crate alloc; -use alloc::vec::Vec; /// PublicKey => User. pub type UsersTable = Database; diff --git a/pubky-homeserver/src/error.rs b/pubky-homeserver/src/error.rs index 46d37d6..b6e5a14 100644 --- a/pubky-homeserver/src/error.rs +++ b/pubky-homeserver/src/error.rs @@ -5,8 +5,6 @@ use axum::{ http::StatusCode, response::IntoResponse, }; -use pubky_common::auth::AuthnSignatureError; -use tracing::debug; pub type Result = core::result::Result; @@ -54,32 +52,26 @@ impl IntoResponse for Error { impl From for Error { fn from(error: QueryRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) + Self::new(StatusCode::BAD_REQUEST, error.into()) } } impl From for Error { fn from(error: ExtensionRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) + Self::new(StatusCode::BAD_REQUEST, error.into()) } } impl From for Error { fn from(error: PathRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) - } -} - -impl From for Error { - fn from(error: std::io::Error) -> Self { - Self::new(StatusCode::INTERNAL_SERVER_ERROR, Some(error)) + Self::new(StatusCode::BAD_REQUEST, error.into()) } } // === Pubky specific errors === -impl From for Error { - fn from(error: AuthnSignatureError) -> Self { +impl From for Error { + fn from(error: pubky_common::auth::Error) -> Self { Self::new(StatusCode::BAD_REQUEST, Some(error)) } } @@ -90,10 +82,40 @@ impl From for Error { } } -impl From for Error { - fn from(error: heed::Error) -> Self { - debug!(?error); +// === INTERNAL_SERVER_ERROR === - Self::with_status(StatusCode::INTERNAL_SERVER_ERROR) +impl From for Error { + fn from(error: std::io::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: heed::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: anyhow::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: postcard::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: axum::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From> for Error { + fn from(error: flume::SendError) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) } } diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs index be65f13..567ca6b 100644 --- a/pubky-homeserver/src/extractors.rs +++ b/pubky-homeserver/src/extractors.rs @@ -45,3 +45,32 @@ where Ok(Pubky(public_key)) } } + +pub struct EntryPath(pub(crate) String); + +impl EntryPath { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +#[async_trait] +impl FromRequestParts for EntryPath +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let params: Path> = + parts.extract().await.map_err(IntoResponse::into_response)?; + + // TODO: enforce path limits like no trailing '/' + + let path = params + .get("path") + .ok_or_else(|| (StatusCode::NOT_FOUND, "entry path missing").into_response())?; + + Ok(EntryPath(path.to_string())) + } +} diff --git a/pubky-homeserver/src/lib.rs b/pubky-homeserver/src/lib.rs index 51852ec..4a1253b 100644 --- a/pubky-homeserver/src/lib.rs +++ b/pubky-homeserver/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(unused)] - pub mod config; mod database; mod error; diff --git a/pubky-homeserver/src/main.rs b/pubky-homeserver/src/main.rs index a54fba4..dad25df 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -1,13 +1,44 @@ +use std::path::PathBuf; + use anyhow::Result; -use pubky_homeserver::Homeserver; +use pubky_homeserver::{config::Config, Homeserver}; + +use clap::Parser; + +#[derive(Parser, Debug)] +struct Cli { + /// [tracing_subscriber::EnvFilter] + #[clap(short, long)] + tracing_env_filter: Option, + + /// Run Homeserver in a local testnet + #[clap(long)] + testnet: bool, + + /// Optional Path to config file. + #[clap(short, long)] + config: Option, +} #[tokio::main] async fn main() -> Result<()> { + let args = Cli::parse(); + tracing_subscriber::fmt() - .with_env_filter("pubky_homeserver=debug,tower_http=debug") + .with_env_filter( + args.tracing_env_filter + .unwrap_or("pubky_homeserver=debug,tower_http=debug".to_string()), + ) .init(); - let server = Homeserver::start(Default::default()).await?; + let server = Homeserver::start(if args.testnet { + Config::testnet() + } else if let Some(config_path) = args.config { + Config::load(config_path).await? + } else { + Config::default() + }) + .await?; server.run_until_done().await?; diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs index 113c598..cf4d7b7 100644 --- a/pubky-homeserver/src/pkarr.rs +++ b/pubky-homeserver/src/pkarr.rs @@ -11,6 +11,8 @@ pub async fn publish_server_packet( domain: &str, port: u16, ) -> anyhow::Result<()> { + // TODO: Try to resolve first before publishing. + let mut packet = Packet::new_reply(0); let mut svcb = SVCB::new(0, domain.try_into()?); diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 86120c2..7422f20 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -1,25 +1,43 @@ use axum::{ + extract::DefaultBodyLimit, routing::{delete, get, post, put}, Router, }; use tower_cookies::CookieManagerLayer; -use tower_http::trace::TraceLayer; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; use crate::server::AppState; +use self::pkarr::pkarr_router; + mod auth; -mod drive; +mod feed; +mod pkarr; +mod public; mod root; -pub fn create_app(state: AppState) -> Router { +fn base(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) - .route("/:pubky", put(auth::signup)) + .route("/signup", post(auth::signup)) + .route("/session", post(auth::signin)) .route("/:pubky/session", get(auth::session)) - .route("/:pubky/session", post(auth::signin)) .route("/:pubky/session", delete(auth::signout)) - .route("/:pubky/*key", get(drive::put)) - .layer(TraceLayer::new_for_http()) + .route("/:pubky/*path", put(public::put)) + .route("/:pubky/*path", get(public::get)) + .route("/:pubky/*path", delete(public::delete)) + .route("/events/", get(feed::feed)) .layer(CookieManagerLayer::new()) + // TODO: revisit if we enable streaming big payloads + // TODO: maybe add to a separate router (drive router?). + .layer(DefaultBodyLimit::max(16 * 1024)) .with_state(state) } + +pub fn create_app(state: AppState) -> Router { + base(state.clone()) + // TODO: Only enable this for test environments? + .nest("/pkarr", pkarr_router(state)) + .layer(CorsLayer::very_permissive()) + .layer(TraceLayer::new_for_http()) +} diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index fceb6fe..dbcffe4 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -1,47 +1,40 @@ use axum::{ - extract::{Request, State}, - http::{HeaderMap, StatusCode}, + debug_handler, + extract::State, + http::{uri::Scheme, StatusCode, Uri}, response::IntoResponse, - routing::get, - Router, }; use axum_extra::{headers::UserAgent, TypedHeader}; use bytes::Bytes; -use heed::BytesEncode; -use postcard::to_allocvec; -use tower_cookies::{Cookie, Cookies}; +use tower_cookies::{cookie::SameSite, Cookie, Cookies}; -use pubky_common::{ - crypto::{random_bytes, random_hash}, - session::Session, - timestamp::Timestamp, -}; +use pubky_common::{crypto::random_bytes, session::Session, timestamp::Timestamp}; use crate::{ database::tables::{ sessions::{SessionsTable, SESSIONS_TABLE}, - users::{User, UsersTable, USERS_TABLE}, + users::User, }, error::{Error, Result}, extractors::Pubky, server::AppState, }; +#[debug_handler] pub async fn signup( State(state): State, - TypedHeader(user_agent): TypedHeader, + user_agent: Option>, cookies: Cookies, - pubky: Pubky, + uri: Uri, body: Bytes, ) -> Result { // TODO: Verify invitation link. // TODO: add errors in case of already axisting user. - signin(State(state), TypedHeader(user_agent), cookies, pubky, body).await + signin(State(state), user_agent, cookies, uri, body).await } pub async fn session( State(state): State, - TypedHeader(user_agent): TypedHeader, cookies: Cookies, pubky: Pubky, ) -> Result { @@ -58,6 +51,7 @@ pub async fn session( let session = session.to_owned(); rtxn.commit()?; + // TODO: add content-type return Ok(session); }; @@ -93,18 +87,18 @@ pub async fn signout( pub async fn signin( State(state): State, - TypedHeader(user_agent): TypedHeader, + user_agent: Option>, cookies: Cookies, - pubky: Pubky, + uri: Uri, body: Bytes, ) -> Result { - let public_key = pubky.public_key(); + let token = state.verifier.verify(&body)?; - state.verifier.verify(&body, public_key)?; + let public_key = token.pubky(); let mut wtxn = state.db.env.write_txn()?; - let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; + let users = state.db.tables.users; if let Some(existing) = users.get(&wtxn, public_key)? { users.put(&mut wtxn, public_key, &existing)?; } else { @@ -119,22 +113,26 @@ pub async fn signin( let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); - let sessions: SessionsTable = state + let session = Session::new(&token, user_agent.map(|ua| ua.to_string())).serialize(); + + state .db - .env - .open_database(&wtxn, Some(SESSIONS_TABLE))? - .expect("Sessions table already created"); + .tables + .sessions + .put(&mut wtxn, &session_secret, &session)?; - // TODO: handle not having a user agent? - let mut session = Session::new(); + let mut cookie = Cookie::new(public_key.to_string(), session_secret); - session.set_user_agent(user_agent.to_string()); + cookie.set_path("/"); + if *uri.scheme().unwrap_or(&Scheme::HTTP) == Scheme::HTTPS { + cookie.set_secure(true); + cookie.set_same_site(SameSite::None); + } + cookie.set_http_only(true); - sessions.put(&mut wtxn, &session_secret, &session.serialize())?; - - cookies.add(Cookie::new(public_key.to_string(), session_secret)); + cookies.add(cookie); wtxn.commit()?; - Ok(()) + Ok(session) } diff --git a/pubky-homeserver/src/routes/drive.rs b/pubky-homeserver/src/routes/drive.rs deleted file mode 100644 index 3050250..0000000 --- a/pubky-homeserver/src/routes/drive.rs +++ /dev/null @@ -1,11 +0,0 @@ -use axum::response::IntoResponse; - -use tracing::debug; - -use crate::extractors::Pubky; - -pub async fn put(pubky: Pubky) -> Result { - debug!(pubky=?pubky.public_key()); - - Ok("Pubky drive...".to_string()) -} diff --git a/pubky-homeserver/src/routes/feed.rs b/pubky-homeserver/src/routes/feed.rs new file mode 100644 index 0000000..bd426f3 --- /dev/null +++ b/pubky-homeserver/src/routes/feed.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use axum::{ + body::Body, + extract::{Query, State}, + http::{header, Response, StatusCode}, + response::IntoResponse, +}; + +use crate::{ + database::{tables::events::Event, MAX_LIST_LIMIT}, + error::Result, + server::AppState, +}; + +pub async fn feed( + State(state): State, + Query(params): Query>, +) -> Result { + let txn = state.db.env.read_txn()?; + + let limit = params + .get("limit") + .and_then(|l| l.parse::().ok()) + .unwrap_or(MAX_LIST_LIMIT) + .min(MAX_LIST_LIMIT); + + let mut cursor = params + .get("cursor") + .map(|c| c.as_str()) + .unwrap_or("0000000000000"); + + // Guard against bad cursor + if cursor.len() < 13 { + cursor = "0000000000000" + } + + let mut result: Vec = vec![]; + let mut next_cursor = cursor.to_string(); + + for _ in 0..limit { + match state + .db + .tables + .events + .get_greater_than(&txn, &next_cursor)? + { + Some((timestamp, event_bytes)) => { + let event = Event::deserialize(event_bytes)?; + + let line = format!("{} {}", event.operation(), event.url()); + next_cursor = timestamp.to_string(); + + result.push(line); + } + None => break, + }; + } + + if !result.is_empty() { + result.push(format!("cursor: {next_cursor}")) + } + + txn.commit()?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/plain") + .body(Body::from(result.join("\n"))) + .unwrap()) +} diff --git a/pubky-homeserver/src/routes/pkarr.rs b/pubky-homeserver/src/routes/pkarr.rs new file mode 100644 index 0000000..9e40230 --- /dev/null +++ b/pubky-homeserver/src/routes/pkarr.rs @@ -0,0 +1,58 @@ +use axum::{ + body::{Body, Bytes}, + extract::State, + http::StatusCode, + response::IntoResponse, + routing::{get, put}, + Router, +}; +use futures_util::stream::StreamExt; + +use pkarr::SignedPacket; + +use crate::{ + error::{Error, Result}, + extractors::Pubky, + server::AppState, +}; + +/// Pkarr relay, helpful for testing. +/// +/// For real productioin, you should use a [production ready +/// relay](https://github.com/pubky/pkarr/server). +pub fn pkarr_router(state: AppState) -> Router { + Router::new() + .route("/:pubky", put(pkarr_put)) + .route("/:pubky", get(pkarr_get)) + .with_state(state) +} + +pub async fn pkarr_put( + State(state): State, + pubky: Pubky, + body: Body, +) -> Result { + let mut bytes = Vec::with_capacity(1104); + + let mut stream = body.into_data_stream(); + + while let Some(chunk) = stream.next().await { + bytes.extend_from_slice(&chunk?) + } + + let public_key = pubky.public_key().to_owned(); + + let signed_packet = SignedPacket::from_relay_payload(&public_key, &Bytes::from(bytes))?; + + state.pkarr_client.publish(&signed_packet).await?; + + Ok(()) +} + +pub async fn pkarr_get(State(state): State, pubky: Pubky) -> Result { + if let Some(signed_packet) = state.pkarr_client.resolve(pubky.public_key()).await? { + return Ok(signed_packet.to_relay_payload()); + } + + Err(Error::with_status(StatusCode::NOT_FOUND)) +} diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs new file mode 100644 index 0000000..4cf2eed --- /dev/null +++ b/pubky-homeserver/src/routes/public.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; + +use axum::{ + body::{Body, Bytes}, + extract::{Query, State}, + http::{header, Response, StatusCode}, + response::IntoResponse, +}; +use futures_util::stream::StreamExt; +use pkarr::PublicKey; +use tower_cookies::Cookies; + +use crate::{ + error::{Error, Result}, + extractors::{EntryPath, Pubky}, + server::AppState, +}; + +pub async fn put( + State(mut state): State, + pubky: Pubky, + path: EntryPath, + cookies: Cookies, + body: Body, +) -> Result { + let public_key = pubky.public_key().clone(); + let path = path.as_str(); + + verify(path)?; + authorize(&mut state, cookies, &public_key, path)?; + + let mut stream = body.into_data_stream(); + + let (tx, rx) = flume::bounded::(1); + + let path = path.to_string(); + + // TODO: refactor Database to clean up this scope. + let done = tokio::task::spawn_blocking(move || -> Result<()> { + // TODO: this is a blocking operation, which is ok for small + // payloads (we have 16 kb limit for now) but later we need + // to stream this to filesystem, and keep track of any failed + // writes to GC these files later. + + state.db.put_entry(&public_key, &path, rx)?; + + Ok(()) + }); + + while let Some(next) = stream.next().await { + let chunk = next?; + + tx.send(chunk)?; + } + + drop(tx); + done.await.expect("join error")?; + + // TODO: return relevant headers, like Etag? + + Ok(()) +} + +pub async fn get( + State(state): State, + pubky: Pubky, + path: EntryPath, + Query(params): Query>, +) -> Result { + verify(path.as_str())?; + let public_key = pubky.public_key(); + + let path = path.as_str(); + + if path.ends_with('/') { + let txn = state.db.env.read_txn()?; + + let path = format!("{public_key}/{path}"); + + if !state.db.contains_directory(&txn, &path)? { + return Err(Error::new( + StatusCode::NOT_FOUND, + "Directory Not Found".into(), + )); + } + + // Handle listing + let vec = state.db.list( + &txn, + &path, + params.contains_key("reverse"), + params.get("limit").and_then(|l| l.parse::().ok()), + params.get("cursor").map(|cursor| cursor.into()), + params.contains_key("shallow"), + )?; + + return Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/plain") + .body(Body::from(vec.join("\n"))) + .unwrap()); + } + + // TODO: Enable streaming + + match state.db.get_blob(public_key, path) { + Err(error) => Err(error)?, + Ok(Some(bytes)) => Ok(Response::builder().body(Body::from(bytes)).unwrap()), + Ok(None) => Err(Error::new(StatusCode::NOT_FOUND, "File Not Found".into())), + } +} + +pub async fn delete( + State(mut state): State, + pubky: Pubky, + path: EntryPath, + cookies: Cookies, +) -> Result { + let public_key = pubky.public_key().clone(); + let path = path.as_str(); + + authorize(&mut state, cookies, &public_key, path)?; + verify(path)?; + + let deleted = state.db.delete_entry(&public_key, path)?; + + if !deleted { + // TODO: if the path ends with `/` return a `CONFLICT` error? + return Err(Error::with_status(StatusCode::NOT_FOUND)); + } + + // TODO: return relevant headers, like Etag? + + Ok(()) +} + +/// Authorize write (PUT or DELETE) for Public paths. +fn authorize( + state: &mut AppState, + cookies: Cookies, + public_key: &PublicKey, + path: &str, +) -> Result<()> { + // TODO: can we move this logic to the extractor or a layer + // to perform this validation? + let session = state + .db + .get_session(cookies, public_key)? + .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; + + if session.pubky() == public_key + && session.capabilities().iter().any(|cap| { + path.starts_with(&cap.scope[1..]) + && cap + .actions + .contains(&pubky_common::capabilities::Action::Write) + }) + { + return Ok(()); + } + + Err(Error::with_status(StatusCode::FORBIDDEN)) +} + +fn verify(path: &str) -> Result<()> { + if !path.starts_with("pub/") { + return Err(Error::new( + StatusCode::FORBIDDEN, + "Writing to directories other than '/pub/' is forbidden".into(), + )); + } + + // TODO: should we forbid paths ending with `/`? + + Ok(()) +} diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index f167d05..c94a803 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,13 +1,13 @@ use std::{future::IntoFuture, net::SocketAddr}; use anyhow::{Error, Result}; -use pubky_common::auth::AuthnVerifier; +use pubky_common::auth::AuthVerifier; use tokio::{net::TcpListener, signal, task::JoinSet}; -use tracing::{info, warn}; +use tracing::{debug, info, warn}; use pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PublicKey, Settings, + PkarrClient, PkarrClientAsync, PublicKey, Settings, }; use crate::{config::Config, database::DB, pkarr::publish_server_packet}; @@ -21,19 +21,33 @@ pub struct Homeserver { #[derive(Clone, Debug)] pub(crate) struct AppState { - pub verifier: AuthnVerifier, + pub verifier: AuthVerifier, pub db: DB, + pub pkarr_client: PkarrClientAsync, } impl Homeserver { pub async fn start(config: Config) -> Result { - let public_key = config.keypair().public_key(); + debug!(?config); + + let keypair = config.keypair(); let db = DB::open(&config.storage()?)?; + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: config.bootstsrap(), + request_timeout: config.dht_request_timeout(), + ..Default::default() + }, + ..Default::default() + })? + .as_async(); + let state = AppState { - verifier: AuthnVerifier::new(public_key.clone()), + verifier: AuthVerifier::default(), db, + pkarr_client: pkarr_client.clone(), }; let app = crate::routes::create_app(state); @@ -58,18 +72,9 @@ impl Homeserver { info!("Homeserver listening on http://localhost:{port}"); - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: config.bootstsrap(), - ..Default::default() - }, - ..Default::default() - })? - .as_async(); + publish_server_packet(pkarr_client, &keypair, config.domain(), port).await?; - publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?; - - info!("Homeserver listening on pubky://{public_key}"); + info!("Homeserver listening on pubky://{}", keypair.public_key()); Ok(Self { tasks, @@ -80,6 +85,8 @@ impl Homeserver { /// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage. pub async fn start_test(testnet: &Testnet) -> Result { + info!("Running testnet.."); + Homeserver::start(Config::test(testnet)).await } diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index e10789a..6871377 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -2,21 +2,43 @@ name = "pubky" version = "0.1.0" edition = "2021" +description = "Pubky client" +license = "MIT" +repository = "https://github.com/pubky/pubky" +keywords = ["web", "dht", "dns", "decentralized", "identity"] + +[lib] +crate-type = ["cdylib", "rlib"] [dependencies] -pubky-common = { version = "0.1.0", path = "../pubky-common" } - -pkarr = "2.1.0" -ureq = { version = "2.10.0", features = ["cookies"] } thiserror = "1.0.62" +wasm-bindgen = "0.2.92" url = "2.5.2" -flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } +bytes = "^1.7.1" +base64 = "0.22.1" + +pubky-common = { version = "0.1.0", path = "../pubky-common" } +pkarr = { workspace = true, features = ["async"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +reqwest = { version = "0.12.5", features = ["cookies", "rustls-tls"], default-features = false } +tokio = { version = "1.37.0", features = ["full"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +reqwest = { version = "0.12.5", default-features = false } + +js-sys = "0.3.69" +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" [features] -async = ["flume/async"] -default = ["async"] +[package.metadata.docs.rs] +all-features = true + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ['-g', '-O'] diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore new file mode 100644 index 0000000..7355b75 --- /dev/null +++ b/pubky/pkg/.gitignore @@ -0,0 +1,6 @@ +index.cjs +browser.js +coverage +node_modules +package-lock.json +pubky* diff --git a/pubky/pkg/LICENSE b/pubky/pkg/LICENSE new file mode 100644 index 0000000..a0e67c5 --- /dev/null +++ b/pubky/pkg/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md new file mode 100644 index 0000000..2228266 --- /dev/null +++ b/pubky/pkg/README.md @@ -0,0 +1,266 @@ +# Pubky + +JavaScript implementation of [Pubky](https://github.com/pubky/pubky). + +## Table of Contents +- [Install](#install) +- [Getting Started](#getting-started) +- [API](#api) +- [Test and Development](#test-and-development) + +## Install + +```bash +npm install @synonymdev/pubky +``` + +### Prerequisites + +For Nodejs, you need Node v20 or later. + +## Getting started + +```js +import { PubkyClient, Keypair, PublicKey } from '../index.js' + +// Initialize PubkyClient with Pkarr relay(s). +let client = new PubkyClient(); + +// Generate a keypair +let keypair = Keypair.random(); + +// Create a new account +let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); + +await client.signup(keypair, homeserver) + +const publicKey = keypair.publicKey(); + +// Pubky URL +let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + +// Verify that you are signed in. +const session = await client.session(publicKey) + +const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + +// PUT public data, by authorized client +await client.put(url, body); + +// GET public data without signup or signin +{ + const client = new PubkyClient(); + + let response = await client.get(url); +} + +// Delete public data, by authorized client +await client.delete(url); +``` + +## API + +### PubkyClient + +#### constructor +```js +let client = new PubkyClient() +``` + +#### signup +```js +await client.signup(keypair, homeserver) +``` +- keypair: An instance of [Keypair](#keypair). +- homeserver: An instance of [PublicKey](#publickey) representing the homeserver. + +Returns: +- session: An instance of [Session](#session). + +#### signin +```js +let session = await client.signin(keypair) +``` +- keypair: An instance of [Keypair](#keypair). + +Returns: +- An instance of [Session](#session). + +#### signout +```js +await client.signout(publicKey) +``` +- publicKey: An instance of [PublicKey](#publicKey). + +#### authRequest +```js +let [pubkyauthUrl, sessionPromise] = client.authRequest(relay, capabilities); + +showQr(pubkyauthUrl); + +let session = await sessionPromise; +``` + +Sign in to a user's Homeserver, without access to their [Keypair](#keypair), nor even [PublicKey](#publickey), +instead request permissions (showing the user pubkyauthUrl), and await a Session after the user consenting to that request. + +- relay: A URL to an [HTTP relay](https://httprelay.io/features/link/) endpoint. +- capabilities: A list of capabilities required for the app for example `/pub/pubky.app/:rw,/pub/example.com/:r`. + +Returns: +- pubkyauthUrl: A url to show to the user to scan or paste into an Authenticator app holding the user [Keypair](#keypair) +- sessionPromise: A promise that resolves into a [Session](#session) on success. + +#### sendAuthToken +```js +await client.sendAuthToken(keypair, pubkyauthUrl); +``` +Consenting to authentication or authorization according to the required capabilities in the `pubkyauthUrl` , and sign and send an auth token to the requester. + +- keypair: An instance of [KeyPair](#keypair) +- pubkyauthUrl: A string `pubkyauth://` url + +#### session {#session-method} +```js +let session = await client.session(publicKey) +``` +- publicKey: An instance of [PublicKey](#publickey). +- Returns: A [Session](#session) object if signed in, or undefined if not. + +#### put +```js +let response = await client.put(url, body); +``` +- url: A string representing the Pubky URL. +- body: A Buffer containing the data to be stored. + +### get +```js +let response = await client.get(url) +``` +- url: A string representing the Pubky URL. +- Returns: A response object containing the requested data. + +### delete + +```js +let response = await client.delete(url); +``` +- url: A string representing the Pubky URL. + +### list +```js +let response = await client.list(url, cursor, reverse, limit) +``` +- url: A string representing the Pubky URL. The path in that url is the prefix that you want to list files within. +- cursor: Usually the last URL from previous calls. List urls after/before (depending on `reverse`) the cursor. +- reverse: Whether or not return urls in reverse order. +- limit: Number of urls to return. +- Returns: A list of URLs of the files in the `url` you passed. + +### Keypair + +#### random +```js +let keypair = Keypair.random() +``` +- Returns: A new random Keypair. + +#### fromSecretKey +```js +let keypair = Keypair.fromSecretKey(secretKey) +``` +- secretKey: A 32 bytes Uint8array. +- Returns: A new Keypair. + + +#### publicKey {#publickey-method} +```js +let publicKey = keypair.publicKey() +``` +- Returns: The [PublicKey](#publickey) associated with the Keypair. + +#### secretKey +```js +let secretKey = keypair.secretKey() +``` +- Returns: The Uint8array secret key associated with the Keypair. + +### PublicKey + +#### from + +```js +let publicKey = PublicKey.from(string); +``` +- string: A string representing the public key. +- Returns: A new PublicKey instance. + +#### z32 +```js +let pubky = publicKey.z32(); +``` +Returns: The z-base-32 encoded string representation of the PublicKey. + +### Session + +#### pubky +```js +let pubky = session.pubky(); +``` +Returns an instance of [PublicKey](#publickey) + +#### capabilities +```js +let capabilities = session.capabilities(); +``` +Returns an array of capabilities, for example `["/pub/pubky.app/:rw"]` + +### Helper functions + +#### createRecoveryFile +```js +let recoveryFile = createRecoveryFile(keypair, passphrase) +``` +- keypair: An instance of [Keypair](#keypair). +- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). +- Returns: A recovery file with a spec line and an encrypted secret key. + +#### createRecoveryFile +```js +let keypair = decryptRecoveryfile(recoveryFile, passphrase) +``` +- recoveryFile: An instance of Uint8Array containing the recovery file blob. +- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). +- Returns: An instance of [Keypair](#keypair). + +## Test and Development + +For test and development, you can run a local homeserver in a test network. + +If you don't have Cargo Installed, start by installing it: + +```bash +curl https://sh.rustup.rs -sSf | sh +``` + +Clone the Pubky repository: + +```bash +git clone https://github.com/pubky/pubky +cd pubky/pkg +``` + +Run the local testnet server + +```bash +npm run testnet +``` + +Use the logged addresses as inputs to `PubkyClient` + +```js +import { PubkyClient } from '../index.js' + +const client = PubkyClient().testnet(); +``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json new file mode 100644 index 0000000..c612adf --- /dev/null +++ b/pubky/pkg/package.json @@ -0,0 +1,41 @@ +{ + "name": "@synonymdev/pubky", + "type": "module", + "description": "Pubky client", + "version": "0.1.15", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/pubky/pubky" + }, + "scripts": { + "testnet": "cargo run -p pubky_homeserver -- --testnet", + "test": "npm run test-nodejs && npm run test-browser", + "test-nodejs": "tape test/*.js -cov", + "test-browser": "browserify test/*.js -p esmify | npx tape-run", + "build": "cargo run --bin bundle_pubky_npm", + "prepublishOnly": "npm run build && npm run test" + }, + "files": [ + "index.cjs", + "browser.js", + "pubky.d.ts", + "pubky_bg.wasm" + ], + "main": "index.cjs", + "browser": "browser.js", + "types": "pubky.d.ts", + "keywords": [ + "web", + "dht", + "dns", + "decentralized", + "identity" + ], + "devDependencies": { + "browser-resolve": "^2.0.0", + "esmify": "^2.1.1", + "tape": "^5.8.1", + "tape-run": "^11.0.0" + } +} diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js new file mode 100644 index 0000000..2207946 --- /dev/null +++ b/pubky/pkg/test/auth.js @@ -0,0 +1,63 @@ +import test from 'tape' + +import { PubkyClient, Keypair, PublicKey } from '../index.cjs' + +const Homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + +test('auth', async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.publicKey() + + await client.signup(keypair, Homeserver) + + const session = await client.session(publicKey) + t.ok(session, "signup") + + { + await client.signout(publicKey) + + const session = await client.session(publicKey) + t.notOk(session, "singout") + } + + { + await client.signin(keypair) + + const session = await client.session(publicKey) + t.ok(session, "signin") + } +}) + +test("3rd party signin", async (t) => { + let keypair = Keypair.random(); + let pubky = keypair.publicKey().z32(); + + // Third party app side + let capabilities = "/pub/pubky.app/:rw,/pub/foo.bar/file:r"; + let client = PubkyClient.testnet(); + let [pubkyauth_url, pubkyauthResponse] = client + .authRequest("https://demo.httprelay.io/link", capabilities); + + if (globalThis.document) { + // Skip `sendAuthToken` in browser + // TODO: figure out why does it fail in browser unit tests + // but not in real browser (check pubky-auth-widget.js commented part) + return + } + + // Authenticator side + { + let client = PubkyClient.testnet(); + + await client.signup(keypair, Homeserver); + + await client.sendAuthToken(keypair, pubkyauth_url) + } + + let session = await pubkyauthResponse; + + t.is(session.pubky().z32(), pubky) + t.deepEqual(session.capabilities(), capabilities.split(',')) +}) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js new file mode 100644 index 0000000..d036862 --- /dev/null +++ b/pubky/pkg/test/keys.js @@ -0,0 +1,21 @@ +import test from 'tape' + +import { Keypair } from '../index.cjs' + +test('generate keys from a seed', async (t) => { + const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex') + + const keypair = Keypair.fromSecretKey(secretkey) + + const publicKey = keypair.publicKey() + + t.is(publicKey.z32(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') +}) + +test('fromSecretKey error', async (t) => { + const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b', 'hex') + + + t.throws(() => Keypair.fromSecretKey(null), /Expected secret_key to be an instance of Uint8Array/) + t.throws(() => Keypair.fromSecretKey(secretkey), /Expected secret_key to be 32 bytes, got 15/) +}) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js new file mode 100644 index 0000000..ec30bb2 --- /dev/null +++ b/pubky/pkg/test/public.js @@ -0,0 +1,351 @@ +import test from 'tape' + +import { PubkyClient, Keypair, PublicKey } from '../index.cjs' + +const Homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); + +test('public: put/get', async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random(); + + await client.signup(keypair, Homeserver); + + const publicKey = keypair.publicKey(); + + let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + // PUT public data, by authorized client + await client.put(url, body); + + const otherClient = PubkyClient.testnet(); + + // GET public data without signup or signin + { + let response = await otherClient.get(url); + + t.ok(Buffer.from(response).equals(body)) + } + + // DELETE public data, by authorized client + await client.delete(url); + + + // GET public data without signup or signin + { + let response = await otherClient.get(url); + + t.notOk(response) + } +}) + +test("not found", async (t) => { + const client = PubkyClient.testnet(); + + + const keypair = Keypair.random(); + + await client.signup(keypair, Homeserver); + + const publicKey = keypair.publicKey(); + + let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + + let result = await client.get(url).catch(e => e); + + t.notOk(result); +}) + +test("unauthorized", async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.publicKey() + + await client.signup(keypair, Homeserver) + + const session = await client.session(publicKey) + t.ok(session, "signup") + + await client.signout(publicKey) + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + + // PUT public data, by authorized client + let result = await client.put(url, body).catch(e => e); + + t.ok(result instanceof Error); + t.is( + result.message, + `HTTP status client error (401 Unauthorized) for url (http://localhost:15411/${publicKey.z32()}/pub/example.com/arbitrary)` + ) +}) + +test("forbidden", async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.publicKey() + + await client.signup(keypair, Homeserver) + + const session = await client.session(publicKey) + t.ok(session, "signup") + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + let url = `pubky://${publicKey.z32()}/priv/example.com/arbitrary`; + + // PUT public data, by authorized client + let result = await client.put(url, body).catch(e => e); + + t.ok(result instanceof Error); + t.is( + result.message, + `HTTP status client error (403 Forbidden) for url (http://localhost:15411/${publicKey.z32()}/priv/example.com/arbitrary)` + ) +}) + +test("list", async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.publicKey() + const pubky = publicKey.z32() + + await client.signup(keypair, Homeserver) + + + + let urls = [ + `pubky://${pubky}/pub/a.wrong/a.txt`, + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.wrong/a.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/z.wrong/a.txt`, + ] + + for (let url of urls) { + await client.put(url, Buffer.from("")); + } + + let url = `pubky://${pubky}/pub/example.com/`; + + { + let list = await client.list(url); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/d.txt`, + + ], + "normal list with no limit or cursor" + ); + } + + { + let list = await client.list(url, null, null, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + + ], + "normal list with limit but no cursor" + ); + } + + { + let list = await client.list(url, "a.txt", null, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + + ], + "normal list with limit and a suffix cursor" + ); + } + + { + let list = await client.list(url, `pubky://${pubky}/pub/example.com/a.txt`, null, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + + ], + "normal list with limit and a full url cursor" + ); + } + + + { + let list = await client.list(url, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/a.txt`, + + ], + "reverse list with no limit or cursor" + ); + } + + { + let list = await client.list(url, null, true, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + + ], + "reverse list with limit but no cursor" + ); + } + + { + let list = await client.list(url, "d.txt", true, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + + ], + "reverse list with limit and a suffix cursor" + ); + } +}) + +test('list shallow', async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.publicKey() + const pubky = publicKey.z32() + + await client.signup(keypair, Homeserver) + + let urls = [ + `pubky://${pubky}/pub/a.com/a.txt`, + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/example.con/d.txt`, + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/z.com/a.txt`, + ] + + for (let url of urls) { + await client.put(url, Buffer.from("")); + } + + let url = `pubky://${pubky}/pub/`; + + { + let list = await client.list(url, null, false, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/a.com/`, + `pubky://${pubky}/pub/example.com/`, + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/example.con/`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/z.com/`, + ], + "normal list shallow" + ); + } + + { + let list = await client.list(url, null, false, 3, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/a.com/`, + `pubky://${pubky}/pub/example.com/`, + `pubky://${pubky}/pub/example.con`, + ], + "normal list shallow with limit" + ); + } + + { + let list = await client.list(url, `example.com/`, false, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/example.con/`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/z.com/`, + ], + "normal list shallow with cursor" + ); + } + + { + let list = await client.list(url, null, true, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/z.com/`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/example.con/`, + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/example.com/`, + `pubky://${pubky}/pub/a.com/`, + ], + "normal list shallow" + ); + } + + { + let list = await client.list(url, null, true, 3, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/z.com/`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/file`, + ], + "normal list shallow with limit" + ); + } +}) diff --git a/pubky/pkg/test/recovery.js b/pubky/pkg/test/recovery.js new file mode 100644 index 0000000..0c033e5 --- /dev/null +++ b/pubky/pkg/test/recovery.js @@ -0,0 +1,19 @@ +import test from 'tape' + +import { Keypair, createRecoveryFile, decryptRecoveryFile } from '../index.cjs' + +test('recovery', async (t) => { + const keypair = Keypair.random(); + + const recoveryFile = createRecoveryFile(keypair, 'very secure password'); + + t.is(recoveryFile.length, 91) + t.deepEqual( + Array.from(recoveryFile.slice(0, 19)), + [112, 117, 98, 107, 121, 46, 111, 114, 103, 47, 114, 101, 99, 111, 118, 101, 114, 121, 10] + ) + + const recovered = decryptRecoveryFile(recoveryFile, 'very secure password') + + t.is(recovered.publicKey().z32(), keypair.publicKey().z32()) +}) diff --git a/pubky/src/bin/bundle_pubky_npm.rs b/pubky/src/bin/bundle_pubky_npm.rs new file mode 100644 index 0000000..40e9b90 --- /dev/null +++ b/pubky/src/bin/bundle_pubky_npm.rs @@ -0,0 +1,65 @@ +use std::env; +use std::io; +use std::process::{Command, ExitStatus}; + +// If the process hangs, try `cargo clean` to remove all locks. + +fn main() { + println!("Building wasm for pubky..."); + + build_wasm("nodejs").unwrap(); + patch().unwrap(); +} + +fn build_wasm(target: &str) -> io::Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + + let output = Command::new("wasm-pack") + .args([ + "build", + &manifest_dir, + "--release", + "--target", + target, + "--out-dir", + &format!("pkg/{}", target), + ]) + .output()?; + + println!( + "wasm-pack {target} output: {}", + String::from_utf8_lossy(&output.stdout) + ); + + if !output.status.success() { + eprintln!( + "wasm-pack failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output.status) +} + +fn patch() -> io::Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + + println!("{manifest_dir}/src/bin/patch.mjs"); + let output = Command::new("node") + .args([format!("{manifest_dir}/src/bin/patch.mjs")]) + .output()?; + + println!( + "patch.mjs output: {}", + String::from_utf8_lossy(&output.stdout) + ); + + if !output.status.success() { + eprintln!( + "patch.mjs failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output.status) +} diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs new file mode 100644 index 0000000..a8ed503 --- /dev/null +++ b/pubky/src/bin/patch.mjs @@ -0,0 +1,66 @@ +// This script is used to generate isomorphic code for web and nodejs +// +// Based on hacks from [this issue](https://github.com/rustwasm/wasm-pack/issues/1334) + +import { readFile, writeFile, rename } from "node:fs/promises"; +import { fileURLToPath } from 'node:url'; +import path, { dirname } from 'node:path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const cargoTomlContent = await readFile(path.join(__dirname, "../../Cargo.toml"), "utf8"); +const cargoPackageName = /\[package\]\nname = "(.*?)"/.exec(cargoTomlContent)[1] +const name = cargoPackageName.replace(/-/g, '_') + +const content = await readFile(path.join(__dirname, `../../pkg/nodejs/${name}.js`), "utf8"); + +const patched = content + // use global TextDecoder TextEncoder + .replace("require(`util`)", "globalThis") + // attach to `imports` instead of module.exports + .replace("= module.exports", "= imports") + // Export classes + .replace(/\nclass (.*?) \{/g, "\n export class $1 {") + // Export functions + .replace(/\nmodule.exports.(.*?) = function/g, "\nimports.$1 = $1;\nexport function $1") + // Add exports to 'imports' + .replace(/\nmodule\.exports\.(.*?)\s+/g, "\nimports.$1") + // Export default + .replace(/$/, 'export default imports') + // inline wasm bytes + .replace( + /\nconst path.*\nconst bytes.*\n/, + ` +var __toBinary = /* @__PURE__ */ (() => { + var table = new Uint8Array(128); + for (var i = 0; i < 64; i++) + table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i; + return (base64) => { + var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0); + for (var i2 = 0, j = 0; i2 < n; ) { + var c0 = table[base64.charCodeAt(i2++)], c1 = table[base64.charCodeAt(i2++)]; + var c2 = table[base64.charCodeAt(i2++)], c3 = table[base64.charCodeAt(i2++)]; + bytes[j++] = c0 << 2 | c1 >> 4; + bytes[j++] = c1 << 4 | c2 >> 2; + bytes[j++] = c2 << 6 | c3; + } + return bytes; + }; +})(); + +const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `../../pkg/nodejs/${name}_bg.wasm`), "base64")) + }); +`, + ); + +await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched + "\nglobalThis['pubky'] = imports"); + +// Move outside of nodejs + +await Promise.all([".js", ".d.ts", "_bg.wasm"].map(suffix => + rename( + path.join(__dirname, `../../pkg/nodejs/${name}${suffix}`), + path.join(__dirname, `../../pkg/${suffix === '.js' ? "index.cjs" : (name + suffix)}`), + )) +) diff --git a/pubky/src/client.rs b/pubky/src/client.rs deleted file mode 100644 index e353e98..0000000 --- a/pubky/src/client.rs +++ /dev/null @@ -1,311 +0,0 @@ -use std::{collections::HashMap, fmt::format, time::Duration}; - -use pkarr::{ - dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, PublicKey, Settings, SignedPacket, -}; -use ureq::{Agent, Response}; -use url::Url; - -use pubky_common::{auth::AuthnSignature, session::Session}; - -use crate::error::{Error, Result}; - -const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; - -#[derive(Debug, Clone)] -pub struct PubkyClient { - agent: Agent, - pkarr: PkarrClient, -} - -impl PubkyClient { - pub fn new() -> Self { - Self { - agent: Agent::new(), - pkarr: PkarrClient::new(Default::default()).unwrap(), - } - } - - pub fn test(testnet: &Testnet) -> Self { - Self { - agent: Agent::new(), - pkarr: PkarrClient::new(Settings { - dht: DhtSettings { - request_timeout: Some(Duration::from_millis(10)), - bootstrap: Some(testnet.bootstrap.to_owned()), - ..DhtSettings::default() - }, - ..Settings::default() - }) - .unwrap(), - } - } - - // === Public Methods === - - /// Signup to a homeserver and update Pkarr accordingly. - /// - /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key - /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" - pub fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (audience, mut url) = self.resolve_endpoint(homeserver)?; - - url.set_path(&format!("/{}", keypair.public_key())); - - self.request(HttpMethod::Put, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) - .map_err(Box::new)?; - - self.publish_pubky_homeserver(keypair, homeserver); - - Ok(()) - } - - /// Check the current sesison for a given Pubky in its homeserver. - pub fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; - - url.set_path(&format!("/{}/session", pubky)); - - let mut bytes = vec![]; - - let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new); - - if let Ok(reader) = result { - reader.into_reader().read_to_end(&mut bytes); - } else { - return Err(Error::NotSignedIn); - } - - Ok(Session::deserialize(&bytes)?) - } - - /// Signout from a homeserver. - pub fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; - - url.set_path(&format!("/{}/session", pubky)); - - self.request(HttpMethod::Delete, &url) - .call() - .map_err(Box::new)?; - - Ok(()) - } - - /// Signin to a homeserver. - pub fn signin(&self, keypair: &Keypair) -> Result<()> { - let pubky = keypair.public_key(); - - let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?; - - url.set_path(&format!("/{}/session", &pubky)); - - self.request(HttpMethod::Post, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) - .map_err(Box::new)?; - - Ok(()) - } - - // === Private Methods === - - /// Publish the SVCB record for `_pubky.`. - pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let mut packet = Packet::new_reply(0); - - if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let svcb = SVCB::new(0, host.try_into()?); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; - - self.pkarr.publish(&signed_packet)?; - - Ok(()) - } - - /// Resolve the homeserver for a pubky. - pub(crate) fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, Url)> { - let target = format!("_pubky.{}", pubky); - - self.resolve_endpoint(&target) - .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) - } - - /// Resolve a service's public_key and clearnet url from a Pubky domain - fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { - // TODO: cache the result of this function? - // TODO: use MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION - // TODO: move to common? - - let mut target = target.to_string(); - let mut homeserver_public_key = None; - let mut host = target.clone(); - - // PublicKey is very good at extracting the Pkarr TLD from a string. - while let Ok(public_key) = PublicKey::try_from(target.clone()) { - if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { - let mut prior = None; - - for answer in signed_packet.resource_records(&target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) - } - // TODO return random if priority is the same - } else { - prior = Some(svcb) - } - } - } - - if let Some(svcb) = prior { - homeserver_public_key = Some(public_key); - target = svcb.target.to_string(); - - if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { - if port.len() < 2 { - // TODO: debug! Error encoding port! - } - let port = u16::from_be_bytes([port[0], port[1]]); - - host = format!("{target}:{port}"); - } else { - host.clone_from(&target); - }; - - continue; - } - }; - - break; - } - - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; - - return Ok((homeserver, Url::parse(&url)?)); - } - - Err(Error::Generic("Could not resolve endpoint".to_string())) - } - - fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request { - self.agent.request_url(method.into(), url) - } -} - -impl Default for PubkyClient { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug, Clone)] -pub enum HttpMethod { - Get, - Put, - Post, - Delete, -} - -impl From for &str { - fn from(value: HttpMethod) -> Self { - match value { - HttpMethod::Get => "GET", - HttpMethod::Put => "PUT", - HttpMethod::Post => "POST", - HttpMethod::Delete => "DELETE", - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use pkarr::{ - dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, Settings, SignedPacket, - }; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn resolve_homeserver() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - // Publish an intermediate controller of the homeserver - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - ..Default::default() - }, - ..Default::default() - }) - .unwrap() - .as_async(); - - let intermediate = Keypair::random(); - - let mut packet = Packet::new_reply(0); - - let server_tld = server.public_key().to_string(); - - let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); - - pkarr_client.publish(&signed_packet).await.unwrap(); - - tokio::task::spawn_blocking(move || { - let client = PubkyClient::test(&testnet); - - let pubky = Keypair::random(); - - client - .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); - - let (public_key, url) = client - .resolve_pubky_homeserver(&pubky.public_key()) - .unwrap(); - - assert_eq!(public_key, server.public_key()); - assert_eq!(url.host_str(), Some("localhost")); - assert_eq!(url.port(), Some(server.port())); - }) - .await - .expect("task failed") - } -} diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs deleted file mode 100644 index de9012c..0000000 --- a/pubky/src/client_async.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::thread; - -use pkarr::{Keypair, PublicKey}; -use pubky_common::session::Session; - -use crate::{error::Result, PubkyClient}; - -pub struct PubkyClientAsync(PubkyClient); - -impl PubkyClient { - pub fn as_async(&self) -> PubkyClientAsync { - PubkyClientAsync(self.clone()) - } -} - -impl PubkyClientAsync { - /// Async version of [PubkyClient::signup] - pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let keypair = keypair.clone(); - let homeserver = homeserver.to_string(); - - thread::spawn(move || sender.send(client.signup(&keypair, &homeserver))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::session] - pub async fn session(&self, pubky: &PublicKey) -> Result { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - - thread::spawn(move || sender.send(client.session(&pubky))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::signout] - pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - - thread::spawn(move || sender.send(client.signout(&pubky))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::signin] - pub async fn signin(&self, keypair: &Keypair) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let keypair = keypair.clone(); - - thread::spawn(move || sender.send(client.signin(&keypair))); - - receiver.recv_async().await? - } -} diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 026382e..c8d80e1 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -12,8 +12,11 @@ pub enum Error { #[error("Generic error: {0}")] Generic(String), - #[error("Not signed in")] - NotSignedIn, + #[error("Could not resolve endpoint for {0}")] + ResolveEndpoint(String), + + #[error("Could not convert the passed type into a Url")] + InvalidUrl, // === Transparent === #[error(transparent)] @@ -22,15 +25,32 @@ pub enum Error { #[error(transparent)] Pkarr(#[from] pkarr::Error), - #[error(transparent)] - Flume(#[from] flume::RecvError), - - #[error(transparent)] - Ureq(#[from] Box), - #[error(transparent)] Url(#[from] url::ParseError), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] Session(#[from] pubky_common::session::Error), + + #[error(transparent)] + Crypto(#[from] pubky_common::crypto::Error), + + #[error(transparent)] + RecoveryFile(#[from] pubky_common::recovery_file::Error), + + #[error(transparent)] + AuthToken(#[from] pubky_common::auth::Error), +} + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; + +#[cfg(target_arch = "wasm32")] +impl From for JsValue { + fn from(error: Error) -> JsValue { + let error_message = error.to_string(); + js_sys::Error::new(&error_message).into() + } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index b05d067..2b6cf42 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -1,59 +1,36 @@ -#![allow(unused)] - -mod client; -mod client_async; mod error; +mod shared; + +#[cfg(not(target_arch = "wasm32"))] +mod native; + +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +use std::{ + collections::HashSet, + sync::{Arc, RwLock}, +}; + +use wasm_bindgen::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +use ::pkarr::PkarrClientAsync; -pub use client::PubkyClient; pub use error::Error; -#[cfg(test)] -mod tests { - use super::*; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::shared::list_builder::ListBuilder; - use super::error::Error; - - use pkarr::{mainline::Testnet, Keypair}; - use pubky_common::session::Session; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn basic_authn() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - let client = PubkyClient::test(&testnet).as_async(); - - let keypair = Keypair::random(); - - client - .signup(&keypair, &server.public_key().to_string()) - .await - .unwrap(); - - let session = client.session(&keypair.public_key()).await.unwrap(); - - assert_eq!(session, Session { ..session.clone() }); - - client.signout(&keypair.public_key()).await.unwrap(); - - { - let session = client.session(&keypair.public_key()).await; - - assert!(session.is_err()); - - match session { - Err(Error::NotSignedIn) => {} - _ => assert!(false, "expected NotSignedInt error"), - } - } - - client.signin(&keypair).await.unwrap(); - - { - let session = client.session(&keypair.public_key()).await.unwrap(); - - assert_eq!(session, Session { ..session.clone() }); - } - } +#[derive(Debug, Clone)] +#[wasm_bindgen] +pub struct PubkyClient { + http: reqwest::Client, + #[cfg(not(target_arch = "wasm32"))] + pub(crate) pkarr: PkarrClientAsync, + /// A cookie jar for nodejs fetch. + #[cfg(target_arch = "wasm32")] + pub(crate) session_cookies: Arc>>, + #[cfg(target_arch = "wasm32")] + pub(crate) pkarr_relays: Vec, } diff --git a/pubky/src/native.rs b/pubky/src/native.rs new file mode 100644 index 0000000..ba0f086 --- /dev/null +++ b/pubky/src/native.rs @@ -0,0 +1,251 @@ +use std::net::ToSocketAddrs; +use std::time::Duration; + +use bytes::Bytes; +use pubky_common::{ + capabilities::Capabilities, + recovery_file::{create_recovery_file, decrypt_recovery_file}, + session::Session, +}; +use reqwest::{RequestBuilder, Response}; +use tokio::sync::oneshot; +use url::Url; + +use pkarr::Keypair; + +use ::pkarr::{mainline::dht::Testnet, PkarrClient, PublicKey, SignedPacket}; + +use crate::{ + error::{Error, Result}, + shared::list_builder::ListBuilder, + PubkyClient, +}; + +static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +#[derive(Debug, Default)] +pub struct PubkyClientBuilder { + pkarr_settings: pkarr::Settings, +} + +impl PubkyClientBuilder { + /// Set Pkarr client [pkarr::Settings]. + pub fn pkarr_settings(mut self, settings: pkarr::Settings) -> Self { + self.pkarr_settings = settings; + self + } + + /// Use the bootstrap nodes of a testnet, as the bootstrap nodes and + /// resolvers in the internal Pkarr client. + pub fn testnet(mut self, testnet: &Testnet) -> Self { + self.pkarr_settings.dht.bootstrap = testnet.bootstrap.to_vec().into(); + + self.pkarr_settings.resolvers = testnet + .bootstrap + .iter() + .flat_map(|resolver| resolver.to_socket_addrs()) + .flatten() + .collect::>() + .into(); + + self + } + + /// Set the request_timeout of the UDP socket in the Mainline DHT client in + /// the internal Pkarr client. + /// + /// Useful to speed unit tests. + /// Defaults to 2 seconds. + pub fn dht_request_timeout(mut self, timeout: Duration) -> Self { + self.pkarr_settings.dht.request_timeout = timeout.into(); + self + } + + /// Build [PubkyClient] + pub fn build(self) -> PubkyClient { + PubkyClient { + http: reqwest::Client::builder() + .cookie_store(true) + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(), + pkarr: PkarrClient::new(self.pkarr_settings).unwrap().as_async(), + } + } +} + +impl Default for PubkyClient { + fn default() -> Self { + PubkyClient::builder().build() + } +} + +// === Public API === + +impl PubkyClient { + /// Returns a builder to edit settings before creating [PubkyClient]. + pub fn builder() -> PubkyClientBuilder { + PubkyClientBuilder::default() + } + + /// Create a client connected to the local network + /// with the bootstrapping node: `localhost:6881` + pub fn testnet() -> Self { + Self::test(&Testnet { + bootstrap: vec!["localhost:6881".to_string()], + nodes: vec![], + }) + } + + /// Creates a [PubkyClient] with: + /// - DHT bootstrap nodes set to the `testnet` bootstrap nodes. + /// - DHT request timout set to 500 milliseconds. (unless in CI, then it is left as default 2000) + /// + /// For more control, you can use [PubkyClientBuilder::testnet] + pub fn test(testnet: &Testnet) -> PubkyClient { + let mut builder = PubkyClient::builder().testnet(testnet); + + if std::env::var("CI").is_err() { + builder = builder.dht_request_timeout(Duration::from_millis(500)); + } + + builder.build() + } + + // === Auth === + + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result { + self.inner_signup(keypair, homeserver).await + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), + /// or [reqwest::Error] if the response has any other `>=400` status code. + pub async fn session(&self, pubky: &PublicKey) -> Result> { + self.inner_session(pubky).await + } + + /// Signout from a homeserver. + pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { + self.inner_signout(pubky).await + } + + /// Signin to a homeserver. + pub async fn signin(&self, keypair: &Keypair) -> Result { + self.inner_signin(keypair).await + } + + // === Public data === + + /// Upload a small payload to a given path. + pub async fn put>(&self, url: T, content: &[u8]) -> Result<()> { + self.inner_put(url, content).await + } + + /// Download a small payload from a given path relative to a pubky author. + pub async fn get>(&self, url: T) -> Result> { + self.inner_get(url).await + } + + /// Delete a file at a path relative to a pubky author. + pub async fn delete>(&self, url: T) -> Result<()> { + self.inner_delete(url).await + } + + /// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send]. + /// + /// `url` sets the path you want to lest within. + pub fn list>(&self, url: T) -> Result { + self.inner_list(url) + } + + // === Helpers === + + /// Create a recovery file of the `keypair`, containing the secret key encrypted + /// using the `passphrase`. + pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { + Ok(create_recovery_file(keypair, passphrase)?) + } + + /// Recover a keypair from a recovery file by decrypting the secret key using `passphrase`. + pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { + Ok(decrypt_recovery_file(recovery_file, passphrase)?) + } + + /// Return `pubkyauth://` url and wait for the incoming [AuthToken] + /// verifying that AuthToken, and if capabilities were requested, signing in to + /// the Pubky's homeserver and returning the [Session] information. + pub fn auth_request( + &self, + relay: impl TryInto, + capabilities: &Capabilities, + ) -> Result<(Url, tokio::sync::oneshot::Receiver>)> { + let mut relay: Url = relay + .try_into() + .map_err(|_| Error::Generic("Invalid relay Url".into()))?; + + let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?; + + let (tx, rx) = oneshot::channel::>(); + + let this = self.clone(); + + tokio::spawn(async move { + let to_send = this + .subscribe_to_auth_response(relay, &client_secret) + .await?; + + tx.send(to_send) + .map_err(|_| Error::Generic("Failed to send the session after signing in with token, since the receiver is dropped".into()))?; + + Ok::<(), Error>(()) + }); + + Ok((pubkyauth_url, rx)) + } + + /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the + /// source of the pubkyauth request url. + pub async fn send_auth_token>( + &self, + keypair: &Keypair, + pubkyauth_url: T, + ) -> Result<()> { + let url: Url = pubkyauth_url.try_into().map_err(|_| Error::InvalidUrl)?; + + self.inner_send_auth_token(keypair, url).await?; + + Ok(()) + } +} + +// === Internals === + +impl PubkyClient { + // === Pkarr === + + pub(crate) async fn pkarr_resolve( + &self, + public_key: &PublicKey, + ) -> Result> { + Ok(self.pkarr.resolve(public_key).await?) + } + + pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + Ok(self.pkarr.publish(signed_packet).await?) + } + + // === HTTP === + + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { + self.http.request(method, url) + } + + pub(crate) fn store_session(&self, _: &Response) {} + pub(crate) fn remove_session(&self, _: &PublicKey) {} +} diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs new file mode 100644 index 0000000..88c4259 --- /dev/null +++ b/pubky/src/shared/auth.rs @@ -0,0 +1,343 @@ +use std::collections::HashMap; + +use base64::{alphabet::URL_SAFE, engine::general_purpose::NO_PAD, Engine}; +use reqwest::{Method, StatusCode}; +use url::Url; + +use pkarr::{Keypair, PublicKey}; +use pubky_common::{ + auth::AuthToken, + capabilities::{Capabilities, Capability}, + crypto::{decrypt, encrypt, hash, random_bytes}, + session::Session, +}; + +use crate::{ + error::{Error, Result}, + PubkyClient, +}; + +use super::pkarr::Endpoint; + +impl PubkyClient { + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + pub(crate) async fn inner_signup( + &self, + keypair: &Keypair, + homeserver: &PublicKey, + ) -> Result { + let homeserver = homeserver.to_string(); + + let Endpoint { mut url, .. } = self.resolve_endpoint(&homeserver).await?; + + url.set_path("/signup"); + + let body = AuthToken::sign(keypair, vec![Capability::root()]).serialize(); + + let response = self + .request(Method::POST, url.clone()) + .body(body) + .send() + .await?; + + self.store_session(&response); + + self.publish_pubky_homeserver(keypair, &homeserver).await?; + + let bytes = response.bytes().await?; + + Ok(Session::deserialize(&bytes)?) + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns None if not signed in, or [reqwest::Error] + /// if the response has any other `>=404` status code. + pub(crate) async fn inner_session(&self, pubky: &PublicKey) -> Result> { + let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{}/session", pubky)); + + let res = self.request(Method::GET, url).send().await?; + + if res.status() == StatusCode::NOT_FOUND { + return Ok(None); + } + + if !res.status().is_success() { + res.error_for_status_ref()?; + }; + + let bytes = res.bytes().await?; + + Ok(Some(Session::deserialize(&bytes)?)) + } + + /// Signout from a homeserver. + pub(crate) async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { + let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{}/session", pubky)); + + self.request(Method::DELETE, url).send().await?; + + self.remove_session(pubky); + + Ok(()) + } + + /// Signin to a homeserver. + pub(crate) async fn inner_signin(&self, keypair: &Keypair) -> Result { + let token = AuthToken::sign(keypair, vec![Capability::root()]); + + self.signin_with_authtoken(&token).await + } + + pub(crate) async fn inner_send_auth_token( + &self, + keypair: &Keypair, + pubkyauth_url: Url, + ) -> Result<()> { + let query_params: HashMap = + pubkyauth_url.query_pairs().into_owned().collect(); + + let relay = query_params + .get("relay") + .map(|r| url::Url::parse(r).expect("Relay query param to be valid URL")) + .expect("Missing relay query param"); + + let client_secret = query_params + .get("secret") + .map(|s| { + let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD); + let bytes = engine.decode(s).expect("invalid client_secret"); + let arr: [u8; 32] = bytes.try_into().expect("invalid client_secret"); + + arr + }) + .expect("Missing client secret"); + + let capabilities = query_params + .get("caps") + .map(|caps_string| { + caps_string + .split(',') + .filter_map(|cap| Capability::try_from(cap).ok()) + .collect::>() + }) + .unwrap_or_default(); + + let token = AuthToken::sign(keypair, capabilities); + + let encrypted_token = encrypt(&token.serialize(), &client_secret)?; + + let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD); + + let mut callback = relay.clone(); + let mut path_segments = callback.path_segments_mut().unwrap(); + path_segments.pop_if_empty(); + let channel_id = engine.encode(hash(&client_secret).as_bytes()); + path_segments.push(&channel_id); + drop(path_segments); + + self.request(Method::POST, callback) + .body(encrypted_token) + .send() + .await?; + + Ok(()) + } + + pub async fn inner_third_party_signin( + &self, + encrypted_token: &[u8], + client_secret: &[u8; 32], + ) -> Result { + let decrypted = decrypt(encrypted_token, client_secret)?; + let token = AuthToken::deserialize(&decrypted)?; + + self.signin_with_authtoken(&token).await?; + + Ok(token.pubky().to_owned()) + } + + pub async fn signin_with_authtoken(&self, token: &AuthToken) -> Result { + let mut url = Url::parse(&format!("https://{}/session", token.pubky()))?; + + self.resolve_url(&mut url).await?; + + let response = self + .request(Method::POST, url) + .body(token.serialize()) + .send() + .await?; + + self.store_session(&response); + + let bytes = response.bytes().await?; + + Ok(Session::deserialize(&bytes)?) + } + + pub(crate) fn create_auth_request( + &self, + relay: &mut Url, + capabilities: &Capabilities, + ) -> Result<(Url, [u8; 32])> { + let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD); + + let client_secret: [u8; 32] = random_bytes::<32>(); + + let pubkyauth_url = Url::parse(&format!( + "pubkyauth:///?caps={capabilities}&secret={}&relay={relay}", + engine.encode(client_secret) + ))?; + + let mut segments = relay + .path_segments_mut() + .map_err(|_| Error::Generic("Invalid relay".into()))?; + // remove trailing slash if any. + segments.pop_if_empty(); + let channel_id = &engine.encode(hash(&client_secret).as_bytes()); + segments.push(channel_id); + drop(segments); + + Ok((pubkyauth_url, client_secret)) + } + + pub(crate) async fn subscribe_to_auth_response( + &self, + relay: Url, + client_secret: &[u8; 32], + ) -> Result> { + let response = self.http.request(Method::GET, relay).send().await?; + let encrypted_token = response.bytes().await?; + let token_bytes = decrypt(&encrypted_token, client_secret)?; + let token = AuthToken::verify(&token_bytes)?; + + if token.capabilities().is_empty() { + Ok(None) + } else { + let session = self.signin_with_authtoken(&token).await?; + Ok(Some(session)) + } + } +} + +#[cfg(test)] +mod tests { + + use crate::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_common::capabilities::{Capabilities, Capability}; + use pubky_homeserver::Homeserver; + use reqwest::StatusCode; + + #[tokio::test] + async fn basic_authn() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let session = client + .session(&keypair.public_key()) + .await + .unwrap() + .unwrap(); + + assert!(session.capabilities().contains(&Capability::root())); + + client.signout(&keypair.public_key()).await.unwrap(); + + { + let session = client.session(&keypair.public_key()).await.unwrap(); + + assert!(session.is_none()); + } + + client.signin(&keypair).await.unwrap(); + + { + let session = client + .session(&keypair.public_key()) + .await + .unwrap() + .unwrap(); + + assert_eq!(session.pubky(), &keypair.public_key()); + assert!(session.capabilities().contains(&Capability::root())); + } + } + + #[tokio::test] + async fn authz() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let keypair = Keypair::random(); + let pubky = keypair.public_key(); + + // Third party app side + let capabilities: Capabilities = + "/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap(); + let client = PubkyClient::test(&testnet); + let (pubkyauth_url, pubkyauth_response) = client + .auth_request("https://demo.httprelay.io/link", &capabilities) + .unwrap(); + + // Authenticator side + { + let client = PubkyClient::test(&testnet); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + client + .send_auth_token(&keypair, pubkyauth_url) + .await + .unwrap(); + } + + let session = pubkyauth_response.await.unwrap().unwrap(); + + assert_eq!(session.pubky(), &pubky); + assert_eq!(session.capabilities(), &capabilities.0); + + // Test access control enforcement + + client + .put(format!("pubky://{pubky}/pub/pubky.app/foo").as_str(), &[]) + .await + .unwrap(); + + assert_eq!( + client + .put(format!("pubky://{pubky}/pub/pubky.app").as_str(), &[]) + .await + .map_err(|e| match e { + crate::Error::Reqwest(e) => e.status(), + _ => None, + }), + Err(Some(StatusCode::FORBIDDEN)) + ); + + assert_eq!( + client + .put(format!("pubky://{pubky}/pub/foo.bar/file").as_str(), &[]) + .await + .map_err(|e| match e { + crate::Error::Reqwest(e) => e.status(), + _ => None, + }), + Err(Some(StatusCode::FORBIDDEN)) + ); + } +} diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs new file mode 100644 index 0000000..0eaec77 --- /dev/null +++ b/pubky/src/shared/list_builder.rs @@ -0,0 +1,105 @@ +use reqwest::Method; +use url::Url; + +use crate::{error::Result, PubkyClient}; + +#[derive(Debug)] +pub struct ListBuilder<'a> { + url: Url, + reverse: bool, + limit: Option, + cursor: Option<&'a str>, + client: &'a PubkyClient, + shallow: bool, +} + +impl<'a> ListBuilder<'a> { + /// Create a new List request builder + pub(crate) fn new(client: &'a PubkyClient, url: Url) -> Self { + Self { + client, + url, + limit: None, + cursor: None, + reverse: false, + shallow: false, + } + } + + /// Set the `reverse` option. + pub fn reverse(mut self, reverse: bool) -> Self { + self.reverse = reverse; + self + } + + /// Set the `limit` value. + pub fn limit(mut self, limit: u16) -> Self { + self.limit = limit.into(); + self + } + + /// Set the `cursor` value. + /// + /// Either a full `pubky://` Url (from previous list response), + /// or a path (to a file or directory) relative to the `url` + pub fn cursor(mut self, cursor: &'a str) -> Self { + self.cursor = cursor.into(); + self + } + + pub fn shallow(mut self, shallow: bool) -> Self { + self.shallow = shallow; + self + } + + /// Send the list request. + /// + /// Returns a list of Pubky URLs of the files in the path of the `url` + /// respecting [ListBuilder::reverse], [ListBuilder::limit] and [ListBuilder::cursor] + /// options. + pub async fn send(self) -> Result> { + let mut url = self.client.pubky_to_http(self.url).await?; + + if !url.path().ends_with('/') { + let path = url.path().to_string(); + let mut parts = path.split('/').collect::>(); + parts.pop(); + + let path = format!("{}/", parts.join("/")); + + url.set_path(&path) + } + + let mut query = url.query_pairs_mut(); + + if self.reverse { + query.append_key_only("reverse"); + } + + if self.shallow { + query.append_key_only("shallow"); + } + + if let Some(limit) = self.limit { + query.append_pair("limit", &limit.to_string()); + } + + if let Some(cursor) = self.cursor { + query.append_pair("cursor", cursor); + } + + drop(query); + + let response = self.client.request(Method::GET, url).send().await?; + + response.error_for_status_ref()?; + + // TODO: bail on too large files. + let bytes = response.bytes().await?; + + Ok(String::from_utf8_lossy(&bytes) + .lines() + .map(String::from) + .collect()) + } +} diff --git a/pubky/src/shared/mod.rs b/pubky/src/shared/mod.rs new file mode 100644 index 0000000..67b456f --- /dev/null +++ b/pubky/src/shared/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod list_builder; +pub mod pkarr; +pub mod public; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs new file mode 100644 index 0000000..d01eded --- /dev/null +++ b/pubky/src/shared/pkarr.rs @@ -0,0 +1,339 @@ +use url::Url; + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PublicKey, SignedPacket, +}; + +use crate::{ + error::{Error, Result}, + PubkyClient, +}; + +const MAX_ENDPOINT_RESOLUTION_RECURSION: u8 = 3; + +impl PubkyClient { + /// Publish the SVCB record for `_pubky.`. + pub(crate) async fn publish_pubky_homeserver( + &self, + keypair: &Keypair, + host: &str, + ) -> Result<()> { + let existing = self.pkarr_resolve(&keypair.public_key()).await?; + + let mut packet = Packet::new_reply(0); + + if let Some(existing) = existing { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr_publish(&signed_packet).await?; + + Ok(()) + } + + /// Resolve the homeserver for a pubky. + pub(crate) async fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result { + let target = format!("_pubky.{pubky}"); + + self.resolve_endpoint(&target) + .await + .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) + } + + /// Resolve a service's public_key and "non-pkarr url" from a Pubky domain + /// + /// "non-pkarr" url is any URL where the hostname isn't a 52 z-base32 character, + /// usually an IPv4, IPv6 or ICANN domain, but could also be any other unknown hostname. + /// + /// Recursively resolve SVCB and HTTPS endpoints, with [MAX_ENDPOINT_RESOLUTION_RECURSION] limit. + pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result { + let original_target = target; + // TODO: cache the result of this function? + + let mut target = target.to_string(); + + let mut endpoint_public_key = None; + let mut origin = target.clone(); + + let mut step = 0; + + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + if step >= MAX_ENDPOINT_RESOLUTION_RECURSION { + break; + }; + step += 1; + + if let Some(signed_packet) = self + .pkarr_resolve(&public_key) + .await + .map_err(|_| Error::ResolveEndpoint(original_target.into()))? + { + // Choose most prior SVCB record + let svcb = signed_packet.resource_records(&target).fold( + None, + |prev: Option, answer| { + if let Some(svcb) = match &answer.rdata { + pkarr::dns::rdata::RData::SVCB(svcb) => Some(svcb), + pkarr::dns::rdata::RData::HTTPS(curr) => Some(&curr.0), + _ => None, + } { + let curr = svcb.clone(); + + if curr.priority == 0 { + return Some(curr); + } + if let Some(prev) = &prev { + // TODO return random if priority is the same + if curr.priority >= prev.priority { + return Some(curr); + } + } else { + return Some(curr); + } + } + + prev + }, + ); + + if let Some(svcb) = svcb { + endpoint_public_key = Some(public_key.clone()); + target = svcb.target.to_string(); + + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + origin = format!("{target}:{port}"); + } else { + origin.clone_from(&target); + }; + + if step >= MAX_ENDPOINT_RESOLUTION_RECURSION { + continue; + }; + } + } else { + break; + } + } + + if PublicKey::try_from(origin.as_str()).is_ok() { + return Err(Error::ResolveEndpoint(original_target.into())); + } + + if let Some(public_key) = endpoint_public_key { + let url = Url::parse(&format!( + "{}://{}", + if origin.starts_with("localhost") { + "http" + } else { + "https" + }, + origin + ))?; + + return Ok(Endpoint { public_key, url }); + } + + Err(Error::ResolveEndpoint(original_target.into())) + } + + pub(crate) async fn resolve_url(&self, url: &mut Url) -> Result<()> { + if let Some(Ok(pubky)) = url.host_str().map(PublicKey::try_from) { + let Endpoint { url: x, .. } = self.resolve_endpoint(&format!("_pubky.{pubky}")).await?; + + url.set_host(x.host_str())?; + url.set_port(x.port()).expect("should work!"); + url.set_scheme(x.scheme()).expect("should work!"); + }; + + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct Endpoint { + // TODO: we don't use this at all? + pub public_key: PublicKey, + pub url: Url, +} + +#[cfg(test)] +mod tests { + use super::*; + + use pkarr::{ + dns::{ + rdata::{HTTPS, SVCB}, + Packet, + }, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, Settings, SignedPacket, + }; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn resolve_endpoint_https() { + let testnet = Testnet::new(10); + + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let domain = "example.com"; + let mut target; + + // Server + { + let keypair = Keypair::random(); + + let https = HTTPS(SVCB::new(0, domain.try_into().unwrap())); + + let mut packet = Packet::new_reply(0); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "foo".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::HTTPS(https), + )); + + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + target = format!("foo.{}", keypair.public_key()); + } + + // intermediate + { + let keypair = Keypair::random(); + + let svcb = SVCB::new(0, target.as_str().try_into().unwrap()); + + let mut packet = Packet::new_reply(0); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "bar".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + target = format!("bar.{}", keypair.public_key()) + } + + { + let keypair = Keypair::random(); + + let svcb = SVCB::new(0, target.as_str().try_into().unwrap()); + + let mut packet = Packet::new_reply(0); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + target = format!("pubky.{}", keypair.public_key()) + } + + let client = PubkyClient::test(&testnet); + + let endpoint = client.resolve_endpoint(&target).await.unwrap(); + + assert_eq!(endpoint.url.host_str().unwrap(), domain); + } + + #[tokio::test] + async fn resolve_homeserver() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + // Publish an intermediate controller of the homeserver + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let intermediate = Keypair::random(); + + let mut packet = Packet::new_reply(0); + + let server_tld = server.public_key().to_string(); + + let svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + { + let client = PubkyClient::test(&testnet); + + let pubky = Keypair::random(); + + client + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())) + .await + .unwrap(); + + let Endpoint { public_key, url } = client + .resolve_pubky_homeserver(&pubky.public_key()) + .await + .unwrap(); + + assert_eq!(public_key, server.public_key()); + assert_eq!(url.host_str(), Some("localhost")); + assert_eq!(url.port(), Some(server.port())); + } + } +} diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs new file mode 100644 index 0000000..febd1fe --- /dev/null +++ b/pubky/src/shared/public.rs @@ -0,0 +1,768 @@ +use bytes::Bytes; + +use pkarr::PublicKey; +use reqwest::{Method, StatusCode}; +use url::Url; + +use crate::{ + error::{Error, Result}, + PubkyClient, +}; + +use super::{list_builder::ListBuilder, pkarr::Endpoint}; + +impl PubkyClient { + pub(crate) async fn inner_put>(&self, url: T, content: &[u8]) -> Result<()> { + let url = self.pubky_to_http(url).await?; + + let response = self + .request(Method::PUT, url) + .body(content.to_owned()) + .send() + .await?; + + response.error_for_status()?; + + Ok(()) + } + + pub(crate) async fn inner_get>(&self, url: T) -> Result> { + let url = self.pubky_to_http(url).await?; + + let response = self.request(Method::GET, url).send().await?; + + if response.status() == StatusCode::NOT_FOUND { + return Ok(None); + } + + response.error_for_status_ref()?; + + // TODO: bail on too large files. + let bytes = response.bytes().await?; + + Ok(Some(bytes)) + } + + pub(crate) async fn inner_delete>(&self, url: T) -> Result<()> { + let url = self.pubky_to_http(url).await?; + + let response = self.request(Method::DELETE, url).send().await?; + + response.error_for_status_ref()?; + + Ok(()) + } + + pub(crate) fn inner_list>(&self, url: T) -> Result { + Ok(ListBuilder::new( + self, + url.try_into().map_err(|_| Error::InvalidUrl)?, + )) + } + + pub(crate) async fn pubky_to_http>(&self, url: T) -> Result { + let original_url: Url = url.try_into().map_err(|_| Error::InvalidUrl)?; + + let pubky = original_url + .host_str() + .ok_or(Error::Generic("Missing Pubky Url host".to_string()))?; + + if let Ok(public_key) = PublicKey::try_from(pubky) { + let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(&public_key).await?; + + // TODO: remove if we move to subdomains instead of paths. + if original_url.scheme() == "pubky" { + let path = original_url.path_segments(); + + let mut split = url.path_segments_mut().unwrap(); + split.push(pubky); + if let Some(segments) = path { + for segment in segments { + split.push(segment); + } + } + drop(split); + } + + return Ok(url); + } + + Ok(original_url) + } +} + +#[cfg(test)] +mod tests { + + use core::panic; + + use crate::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_homeserver::Homeserver; + use reqwest::{Method, StatusCode}; + + #[tokio::test] + async fn put_get_delete() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let url = format!("pubky://{}/pub/foo.txt", keypair.public_key()); + let url = url.as_str(); + + client.put(url, &[0, 1, 2, 3, 4]).await.unwrap(); + + let response = client.get(url).await.unwrap().unwrap(); + + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); + + client.delete(url).await.unwrap(); + + let response = client.get(url).await.unwrap(); + + assert_eq!(response, None); + } + + #[tokio::test] + async fn unauthorized_put_delete() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let public_key = keypair.public_key(); + + let url = format!("pubky://{public_key}/pub/foo.txt"); + let url = url.as_str(); + + let other_client = PubkyClient::test(&testnet); + { + let other = Keypair::random(); + + // TODO: remove extra client after switching to subdomains. + other_client + .signup(&other, &server.public_key()) + .await + .unwrap(); + + let response = other_client.put(url, &[0, 1, 2, 3, 4]).await; + + match response { + Err(Error::Reqwest(error)) => { + assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) + } + _ => { + panic!("expected error StatusCode::UNAUTHORIZED") + } + } + } + + client.put(url, &[0, 1, 2, 3, 4]).await.unwrap(); + + { + let other = Keypair::random(); + + // TODO: remove extra client after switching to subdomains. + other_client + .signup(&other, &server.public_key()) + .await + .unwrap(); + + let response = other_client.delete(url).await; + + match response { + Err(Error::Reqwest(error)) => { + assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) + } + _ => { + panic!("expected error StatusCode::UNAUTHORIZED") + } + } + } + + let response = client.get(url).await.unwrap().unwrap(); + + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); + } + + #[tokio::test] + async fn list() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let pubky = keypair.public_key(); + + let urls = vec![ + format!("pubky://{pubky}/pub/a.wrong/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.wrong/a.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/z.wrong/a.txt"), + ]; + + for url in urls { + client.put(url.as_str(), &[0]).await.unwrap(); + } + + let url = format!("pubky://{pubky}/pub/example.com/extra"); + let url = url.as_str(); + + { + let list = client.list(url).unwrap().send().await.unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + ], + "normal list with no limit or cursor" + ); + } + + { + let list = client.list(url).unwrap().limit(2).send().await.unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + ], + "normal list with limit but no cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .limit(2) + .cursor("a.txt") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + ], + "normal list with limit and a file cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .limit(2) + .cursor("cc-nested/") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + ], + "normal list with limit and a directory cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .limit(2) + .cursor(&format!("pubky://{pubky}/pub/example.com/a.txt")) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + ], + "normal list with limit and a full url cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .limit(2) + .cursor("/a.txt") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + ], + "normal list with limit and a leading / cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .reverse(true) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + ], + "reverse list with no limit or cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .reverse(true) + .limit(2) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + ], + "reverse list with limit but no cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .reverse(true) + .limit(2) + .cursor("d.txt") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + ], + "reverse list with limit and cursor" + ); + } + } + + #[tokio::test] + async fn list_shallow() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let pubky = keypair.public_key(); + + let urls = vec![ + format!("pubky://{pubky}/pub/a.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.con/d.txt"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/a.txt"), + ]; + + for url in urls { + client.put(url.as_str(), &[0]).await.unwrap(); + } + + let url = format!("pubky://{pubky}/pub/"); + let url = url.as_str(); + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/a.com/"), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/"), + ], + "normal list shallow" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .limit(2) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/a.com/"), + format!("pubky://{pubky}/pub/example.com/"), + ], + "normal list shallow with limit but no cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .limit(2) + .cursor("example.com/a.txt") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/example.con"), + ], + "normal list shallow with limit and a file cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .limit(3) + .cursor("example.com/") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/file"), + ], + "normal list shallow with limit and a directory cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .reverse(true) + .shallow(true) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/z.com/"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/a.com/"), + ], + "reverse list shallow" + ); + } + + { + let list = client + .list(url) + .unwrap() + .reverse(true) + .shallow(true) + .limit(2) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/z.com/"), + format!("pubky://{pubky}/pub/file2"), + ], + "reverse list shallow with limit but no cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .reverse(true) + .limit(2) + .cursor("file2") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/example.con/"), + ], + "reverse list shallow with limit and a file cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .reverse(true) + .limit(2) + .cursor("example.con/") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.com/"), + ], + "reverse list shallow with limit and a directory cursor" + ); + } + } + + #[tokio::test] + async fn list_events() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let pubky = keypair.public_key(); + + let urls = vec![ + format!("pubky://{pubky}/pub/a.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.con/d.txt"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/a.txt"), + ]; + + for url in urls { + client.put(url.as_str(), &[0]).await.unwrap(); + client.delete(url.as_str()).await.unwrap(); + } + + let feed_url = format!("http://localhost:{}/events/", server.port()); + let feed_url = feed_url.as_str(); + + let client = PubkyClient::test(&testnet); + + let cursor; + + { + let response = client + .request( + Method::GET, + format!("{feed_url}?limit=10").as_str().try_into().unwrap(), + ) + .send() + .await + .unwrap(); + + let text = response.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + cursor = lines.last().unwrap().split(" ").last().unwrap().to_string(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{pubky}/pub/a.com/a.txt"), + format!("DEL pubky://{pubky}/pub/a.com/a.txt"), + format!("PUT pubky://{pubky}/pub/example.com/a.txt"), + format!("DEL pubky://{pubky}/pub/example.com/a.txt"), + format!("PUT pubky://{pubky}/pub/example.com/b.txt"), + format!("DEL pubky://{pubky}/pub/example.com/b.txt"), + format!("PUT pubky://{pubky}/pub/example.com/c.txt"), + format!("DEL pubky://{pubky}/pub/example.com/c.txt"), + format!("PUT pubky://{pubky}/pub/example.com/d.txt"), + format!("DEL pubky://{pubky}/pub/example.com/d.txt"), + format!("cursor: {cursor}",) + ] + ); + } + + { + let response = client + .request( + Method::GET, + format!("{feed_url}?limit=10&cursor={cursor}") + .as_str() + .try_into() + .unwrap(), + ) + .send() + .await + .unwrap(); + + let text = response.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{pubky}/pub/example.con/d.txt"), + format!("DEL pubky://{pubky}/pub/example.con/d.txt"), + format!("PUT pubky://{pubky}/pub/example.con"), + format!("DEL pubky://{pubky}/pub/example.con"), + format!("PUT pubky://{pubky}/pub/file"), + format!("DEL pubky://{pubky}/pub/file"), + format!("PUT pubky://{pubky}/pub/file2"), + format!("DEL pubky://{pubky}/pub/file2"), + format!("PUT pubky://{pubky}/pub/z.com/a.txt"), + format!("DEL pubky://{pubky}/pub/z.com/a.txt"), + lines.last().unwrap().to_string() + ] + ) + } + } + + #[tokio::test] + async fn read_after_event() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let pubky = keypair.public_key(); + + let url = format!("pubky://{pubky}/pub/a.com/a.txt"); + + client.put(url.as_str(), &[0]).await.unwrap(); + + let feed_url = format!("http://localhost:{}/events/", server.port()); + let feed_url = feed_url.as_str(); + + let client = PubkyClient::test(&testnet); + + { + let response = client + .request( + Method::GET, + format!("{feed_url}?limit=10").as_str().try_into().unwrap(), + ) + .send() + .await + .unwrap(); + + let text = response.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + let cursor = lines.last().unwrap().split(" ").last().unwrap().to_string(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{pubky}/pub/a.com/a.txt"), + format!("cursor: {cursor}",) + ] + ); + } + + let get = client.get(url.as_str()).await.unwrap(); + dbg!(get); + } +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs new file mode 100644 index 0000000..09dc045 --- /dev/null +++ b/pubky/src/wasm.rs @@ -0,0 +1,255 @@ +use std::{ + collections::HashSet, + sync::{Arc, RwLock}, +}; + +use js_sys::{Array, Uint8Array}; +use wasm_bindgen::prelude::*; + +use url::Url; + +use pubky_common::capabilities::Capabilities; + +use crate::error::Error; +use crate::PubkyClient; + +mod http; +mod keys; +mod pkarr; +mod recovery_file; +mod session; + +use keys::{Keypair, PublicKey}; +use session::Session; + +impl Default for PubkyClient { + fn default() -> Self { + Self::new() + } +} + +static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"]; +static TESTNET_RELAYS: [&str; 1] = ["http://localhost:15411/pkarr"]; + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + http: reqwest::Client::builder().build().unwrap(), + session_cookies: Arc::new(RwLock::new(HashSet::new())), + pkarr_relays: DEFAULT_RELAYS.into_iter().map(|s| s.to_string()).collect(), + } + } + + /// Create a client with with configurations appropriate for local testing: + /// - set Pkarr relays to `["http://localhost:15411/pkarr"]` instead of default relay. + #[wasm_bindgen] + pub fn testnet() -> Self { + Self { + http: reqwest::Client::builder().build().unwrap(), + session_cookies: Arc::new(RwLock::new(HashSet::new())), + pkarr_relays: TESTNET_RELAYS.into_iter().map(|s| s.to_string()).collect(), + } + } + + /// Set Pkarr relays used for publishing and resolving Pkarr packets. + /// + /// By default, [PubkyClient] will use `["https://relay.pkarr.org"]` + #[wasm_bindgen(js_name = "setPkarrRelays")] + pub fn set_pkarr_relays(mut self, relays: Vec) -> Self { + self.pkarr_relays = relays; + self + } + + // Read the set of pkarr relays used by this client. + #[wasm_bindgen(js_name = "getPkarrRelays")] + pub fn get_pkarr_relays(&self) -> Vec { + self.pkarr_relays.clone() + } + + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + #[wasm_bindgen] + pub async fn signup( + &self, + keypair: &Keypair, + homeserver: &PublicKey, + ) -> Result { + Ok(Session( + self.inner_signup(keypair.as_inner(), homeserver.as_inner()) + .await + .map_err(|e| JsValue::from(e))?, + )) + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), + /// or throws the recieved error if the response has any other `>=400` status code. + #[wasm_bindgen] + pub async fn session(&self, pubky: &PublicKey) -> Result, JsValue> { + self.inner_session(pubky.as_inner()) + .await + .map(|s| s.map(Session)) + .map_err(|e| e.into()) + } + + /// Signout from a homeserver. + #[wasm_bindgen] + pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> { + self.inner_signout(pubky.as_inner()) + .await + .map_err(|e| e.into()) + } + + /// Signin to a homeserver using the root Keypair. + #[wasm_bindgen] + pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> { + self.inner_signin(keypair.as_inner()) + .await + .map(|_| ()) + .map_err(|e| e.into()) + } + + /// Return `pubkyauth://` url and wait for the incoming [AuthToken] + /// verifying that AuthToken, and if capabilities were requested, signing in to + /// the Pubky's homeserver and returning the [Session] information. + /// + /// Returns a tuple of [pubkyAuthUrl, Promise] + #[wasm_bindgen(js_name = "authRequest")] + pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result { + let mut relay: Url = relay + .try_into() + .map_err(|_| Error::Generic("Invalid relay Url".into()))?; + + let (pubkyauth_url, client_secret) = self.create_auth_request( + &mut relay, + &Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?, + )?; + + let this = self.clone(); + + let future = async move { + this.subscribe_to_auth_response(relay, &client_secret) + .await + .map(|opt| { + opt.map_or_else( + || JsValue::NULL, // Convert `None` to `JsValue::NULL` + |session| JsValue::from(Session(session)), + ) + }) + .map_err(|err| JsValue::from_str(&format!("{:?}", err))) + }; + + let promise = wasm_bindgen_futures::future_to_promise(future); + + // Return the URL and the promise + let js_tuple = js_sys::Array::new(); + js_tuple.push(&JsValue::from_str(pubkyauth_url.as_ref())); + js_tuple.push(&promise); + + Ok(js_tuple) + } + + /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the + /// source of the pubkyauth request url. + #[wasm_bindgen(js_name = "sendAuthToken")] + pub async fn send_auth_token( + &self, + keypair: &Keypair, + pubkyauth_url: &str, + ) -> Result<(), JsValue> { + let pubkyauth_url: Url = pubkyauth_url + .try_into() + .map_err(|_| Error::Generic("Invalid relay Url".into()))?; + + self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url) + .await?; + + Ok(()) + } + + // === Public data === + + #[wasm_bindgen] + /// Upload a small payload to a given path. + pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> { + self.inner_put(url, content).await.map_err(|e| e.into()) + } + + /// Download a small payload from a given path relative to a pubky author. + #[wasm_bindgen] + pub async fn get(&self, url: &str) -> Result, JsValue> { + self.inner_get(url) + .await + .map(|b| b.map(|b| (&*b).into())) + .map_err(|e| e.into()) + } + + /// Delete a file at a path relative to a pubky author. + #[wasm_bindgen] + pub async fn delete(&self, url: &str) -> Result<(), JsValue> { + self.inner_delete(url).await.map_err(|e| e.into()) + } + + /// Returns a list of Pubky urls (as strings). + /// + /// - `url`: The Pubky url (string) to the directory you want to list its content. + /// - `cursor`: Either a full `pubky://` Url (from previous list response), + /// or a path (to a file or directory) relative to the `url` + /// - `reverse`: List in reverse order + /// - `limit` Limit the number of urls in the response + /// - `shallow`: List directories and files, instead of flat list of files. + #[wasm_bindgen] + pub async fn list( + &self, + url: &str, + cursor: Option, + reverse: Option, + limit: Option, + shallow: Option, + ) -> Result { + // TODO: try later to return Vec from async function. + + if let Some(cursor) = cursor { + return self + .inner_list(url)? + .reverse(reverse.unwrap_or(false)) + .limit(limit.unwrap_or(u16::MAX)) + .cursor(&cursor) + .shallow(shallow.unwrap_or(false)) + .send() + .await + .map(|urls| { + let js_array = Array::new(); + + for url in urls { + js_array.push(&JsValue::from_str(&url)); + } + + js_array + }) + .map_err(|e| e.into()); + } + + self.inner_list(url)? + .reverse(reverse.unwrap_or(false)) + .limit(limit.unwrap_or(u16::MAX)) + .shallow(shallow.unwrap_or(false)) + .send() + .await + .map(|urls| { + let js_array = Array::new(); + + for url in urls { + js_array.push(&JsValue::from_str(&url)); + } + + js_array + }) + .map_err(|e| e.into()) + } +} diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs new file mode 100644 index 0000000..61fee29 --- /dev/null +++ b/pubky/src/wasm/http.rs @@ -0,0 +1,40 @@ +use crate::PubkyClient; + +use reqwest::{Method, RequestBuilder, Response}; +use url::Url; + +impl PubkyClient { + pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { + let mut request = self.http.request(method, url).fetch_credentials_include(); + + for cookie in self.session_cookies.read().unwrap().iter() { + request = request.header("Cookie", cookie); + } + + request + } + + // Support cookies for nodejs + + pub(crate) fn store_session(&self, response: &Response) { + if let Some(cookie) = response + .headers() + .get("set-cookie") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.split(';').next()) + { + self.session_cookies + .write() + .unwrap() + .insert(cookie.to_string()); + } + } + pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { + let key = pubky.to_string(); + + self.session_cookies + .write() + .unwrap() + .retain(|cookie| !cookie.starts_with(&key)); + } +} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs new file mode 100644 index 0000000..3b27045 --- /dev/null +++ b/pubky/src/wasm/keys.rs @@ -0,0 +1,99 @@ +use wasm_bindgen::prelude::*; + +use crate::Error; + +#[wasm_bindgen] +pub struct Keypair(pkarr::Keypair); + +#[wasm_bindgen] +impl Keypair { + #[wasm_bindgen] + /// Generate a random [Keypair] + pub fn random() -> Self { + Self(pkarr::Keypair::random()) + } + + /// Generate a [Keypair] from a secret key. + #[wasm_bindgen(js_name = "fromSecretKey")] + pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Result { + if !js_sys::Uint8Array::instanceof(&secret_key) { + return Err("Expected secret_key to be an instance of Uint8Array".into()); + } + + let len = secret_key.byte_length(); + if len != 32 { + return Err(format!("Expected secret_key to be 32 bytes, got {len}"))?; + } + + let mut bytes = [0; 32]; + secret_key.copy_to(&mut bytes); + + Ok(Self(pkarr::Keypair::from_secret_key(&bytes))) + } + + /// Returns the secret key of this keypair. + #[wasm_bindgen(js_name = "secretKey")] + pub fn secret_key(&self) -> js_sys::Uint8Array { + self.0.secret_key().as_slice().into() + } + + /// Returns the [PublicKey] of this keypair. + #[wasm_bindgen(js_name = "publicKey")] + pub fn public_key(&self) -> PublicKey { + PublicKey(self.0.public_key()) + } +} + +impl Keypair { + pub fn as_inner(&self) -> &pkarr::Keypair { + &self.0 + } +} + +impl From for Keypair { + fn from(keypair: pkarr::Keypair) -> Self { + Self(keypair) + } +} + +#[wasm_bindgen] +pub struct PublicKey(pub(crate) pkarr::PublicKey); + +#[wasm_bindgen] +impl PublicKey { + #[wasm_bindgen] + /// Convert the PublicKey to Uint8Array + pub fn to_uint8array(&self) -> js_sys::Uint8Array { + js_sys::Uint8Array::from(self.0.as_bytes().as_slice()) + } + + #[wasm_bindgen] + /// Returns the z-base32 encoding of this public key + pub fn z32(&self) -> String { + self.0.to_string() + } + + #[wasm_bindgen(js_name = "from")] + /// @throws + pub fn try_from(value: JsValue) -> Result { + let string = value + .as_string() + .ok_or("Couldn't create a PublicKey from this type of value")?; + + Ok(PublicKey( + pkarr::PublicKey::try_from(string).map_err(Error::Pkarr)?, + )) + } +} + +impl PublicKey { + pub fn as_inner(&self) -> &pkarr::PublicKey { + &self.0 + } +} + +impl From for PublicKey { + fn from(value: pkarr::PublicKey) -> Self { + PublicKey(value) + } +} diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs new file mode 100644 index 0000000..49726f6 --- /dev/null +++ b/pubky/src/wasm/pkarr.rs @@ -0,0 +1,48 @@ +use reqwest::StatusCode; + +pub use pkarr::{PublicKey, SignedPacket}; + +use crate::error::Result; +use crate::PubkyClient; + +// TODO: Add an in memory cache of packets + +impl PubkyClient { + //TODO: migrate to pkarr::PkarrRelayClient + pub(crate) async fn pkarr_resolve( + &self, + public_key: &PublicKey, + ) -> Result> { + //TODO: Allow multiple relays in parallel + let relay = self.pkarr_relays.first().expect("initialized with relays"); + + let res = self + .http + .get(format!("{relay}/{}", public_key)) + .send() + .await?; + + if res.status() == StatusCode::NOT_FOUND { + return Ok(None); + }; + + // TODO: guard against too large responses. + let bytes = res.bytes().await?; + + let existing = SignedPacket::from_relay_payload(public_key, &bytes)?; + + Ok(Some(existing)) + } + + pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + let relay = self.pkarr_relays.first().expect("initialized with relays"); + + self.http + .put(format!("{relay}/{}", signed_packet.public_key())) + .body(signed_packet.to_relay_payload()) + .send() + .await?; + + Ok(()) + } +} diff --git a/pubky/src/wasm/recovery_file.rs b/pubky/src/wasm/recovery_file.rs new file mode 100644 index 0000000..7b85178 --- /dev/null +++ b/pubky/src/wasm/recovery_file.rs @@ -0,0 +1,24 @@ +use js_sys::Uint8Array; +use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; + +use crate::error::Error; + +use super::keys::Keypair; + +/// Create a recovery file of the `keypair`, containing the secret key encrypted +/// using the `passphrase`. +#[wasm_bindgen(js_name = "createRecoveryFile")] +pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result { + pubky_common::recovery_file::create_recovery_file(keypair.as_inner(), passphrase) + .map(|b| b.as_slice().into()) + .map_err(|e| Error::from(e).into()) +} + +/// Create a recovery file of the `keypair`, containing the secret key encrypted +/// using the `passphrase`. +#[wasm_bindgen(js_name = "decryptRecoveryFile")] +pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { + pubky_common::recovery_file::decrypt_recovery_file(recovery_file, passphrase) + .map(Keypair::from) + .map_err(|e| Error::from(e).into()) +} diff --git a/pubky/src/wasm/session.rs b/pubky/src/wasm/session.rs new file mode 100644 index 0000000..e838a80 --- /dev/null +++ b/pubky/src/wasm/session.rs @@ -0,0 +1,27 @@ +use pubky_common::session; + +use wasm_bindgen::prelude::*; + +use super::keys::PublicKey; + +#[wasm_bindgen] +pub struct Session(pub(crate) session::Session); + +#[wasm_bindgen] +impl Session { + /// Return the [PublicKey] of this session + #[wasm_bindgen] + pub fn pubky(&self) -> PublicKey { + self.0.pubky().clone().into() + } + + /// Return the capabilities that this session has. + #[wasm_bindgen] + pub fn capabilities(&self) -> Vec { + self.0 + .capabilities() + .iter() + .map(|c| c.to_string()) + .collect() + } +}