diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a72ef83 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3140 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +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.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "async-trait" +version = "0.1.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "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.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +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 = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +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.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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +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 = "clap" +version = "4.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +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" +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 = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +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 = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[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 = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf", +] + +[[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 = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[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 = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[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.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "goblin" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[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 = "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" +dependencies = [ + "bitflags", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3f528b053a6d700b2734eabcd0fd49cb8230647aa72958467527b0b7917114" +dependencies = [ + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + +[[package]] +name = "hermit-abi" +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" +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 = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[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", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +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 = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[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" +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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lmdb-master-sys" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "472c3760e2a8d0f61f322fb36788021bb36d573c502b50fa3e2bcaac3ec326c9" +dependencies = [ + "cc", + "doxygen-rs", + "libc", +] + +[[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 = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[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 = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[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 = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[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 = "oneshot-uniffi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c548d5c78976f6955d72d0ced18c48ca07030f7a1d4024529fedd7c1c01b29c" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[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#5b2f8e53735f63521b0d587167609a2cb9b8f2ab" +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 = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[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 = [ + "base64 0.22.1", + "bytes", + "js-sys", + "pkarr", + "pubky-common", + "reqwest", + "thiserror", + "tokio", + "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", + "rand", + "serde", + "thiserror", +] + +[[package]] +name = "pubky_homeserver" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "axum-extra", + "base32", + "bytes", + "clap", + "dirs-next", + "flume", + "futures-util", + "heed", + "hex", + "pkarr", + "postcard", + "pubky-common", + "serde", + "tokio", + "toml 0.8.19", + "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[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 = "react_native_pubky" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "hex", + "once_cell", + "pkarr", + "pubky", + "pubky-common", + "pubky_homeserver", + "serde", + "serde_json", + "sha2", + "tokio", + "uniffi", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +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", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[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 = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +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.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[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 = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[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 = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[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 = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + +[[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 = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[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", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[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", + "tracing", +] + +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[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 = [ + "log", + "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", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "uniffi" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21345172d31092fd48c47fd56c53d4ae9e41c4b1f559fb8c38c1ab1685fd919f" +dependencies = [ + "anyhow", + "camino", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd992f2929a053829d5875af1eff2ee3d7a7001cb3b9a46cc7895f2caede6940" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "clap", + "fs-err", + "glob", + "goblin", + "heck 0.4.1", + "once_cell", + "paste", + "serde", + "toml 0.5.11", + "uniffi_meta", + "uniffi_testing", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "001964dd3682d600084b3aaf75acf9c3426699bc27b65e96bb32d175a31c74e9" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "uniffi_core" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6121a127a3af1665cd90d12dd2b3683c2643c5103281d0fed5838324ca1fad5b" +dependencies = [ + "anyhow", + "bytes", + "camino", + "log", + "once_cell", + "oneshot-uniffi", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11cf7a58f101fcedafa5b77ea037999b88748607f0ef3a33eaa0efc5392e92e4" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml 0.5.11", + "uniffi_build", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dc8573a7b1ac4b71643d6da34888273ebfc03440c525121f1b3634ad3417a2" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "118448debffcb676ddbe8c5305fb933ab7e0123753e659a71dc4a693f8d9f23c" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "889edb7109c6078abe0e53e9b4070cf74a6b3468d141bdf5ef1bd4d1dc24a1c3" +dependencies = [ + "anyhow", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + +[[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 = "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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[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 = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weedle2" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +dependencies = [ + "nom", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[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", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[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/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..288b335 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "react_native_pubky" +version = "0.1.0" +edition = "2021" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate_type = ["cdylib"] +name = "pubkymobile" + +[[bin]] +name = "testing" +path = "testing/main.rs" + +[net] +git-fetch-with-cli = true + +[dependencies] +uniffi = { version = "0.25.3", features = [ "cli" ] } +serde_json = "1.0.114" +hex = "0.4.3" +sha2 = "0.10.8" +serde = { version = "^1.0.209", features = ["derive"] } +tokio = "1.40.0" +url = "2.5.2" +pkarr = { git = "https://github.com/Pubky/pkarr", branch = "v3", features = ["async", "rand"] } +pubky = { version = "0.1.0", path = "pubky/pubky" } +pubky-common = { version = "0.1.0", path = "pubky/pubky-common" } +pubky_homeserver = { version = "0.1.0", path = "pubky/pubky-homeserver" } +base64 = "0.22.1" +once_cell = "1.19.0" diff --git a/README.md b/README.md index 15c6af5..5b2f97f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ # pubky-core-mobile-sdk Pubky Core Mobile SDK + +### To build both iOS and Android bindings: +``` +./build.sh all +``` + +### To build only iOS bindings: +``` +./build.sh ios +``` + +### To build only Android bindings: +``` +./build.sh android +``` \ No newline at end of file diff --git a/bindings/android/jniLibs/arm64-v8a/libpubkymobile.so b/bindings/android/jniLibs/arm64-v8a/libpubkymobile.so new file mode 100755 index 0000000..103e27f Binary files /dev/null and b/bindings/android/jniLibs/arm64-v8a/libpubkymobile.so differ diff --git a/bindings/android/jniLibs/armeabi-v7a/libpubkymobile.so b/bindings/android/jniLibs/armeabi-v7a/libpubkymobile.so new file mode 100755 index 0000000..f22153b Binary files /dev/null and b/bindings/android/jniLibs/armeabi-v7a/libpubkymobile.so differ diff --git a/bindings/android/jniLibs/x86/libpubkymobile.so b/bindings/android/jniLibs/x86/libpubkymobile.so new file mode 100755 index 0000000..ee77eb3 Binary files /dev/null and b/bindings/android/jniLibs/x86/libpubkymobile.so differ diff --git a/bindings/android/jniLibs/x86_64/libpubkymobile.so b/bindings/android/jniLibs/x86_64/libpubkymobile.so new file mode 100755 index 0000000..fbdfa1d Binary files /dev/null and b/bindings/android/jniLibs/x86_64/libpubkymobile.so differ diff --git a/bindings/android/pubkymobile.kt b/bindings/android/pubkymobile.kt new file mode 100644 index 0000000..832b931 --- /dev/null +++ b/bindings/android/pubkymobile.kt @@ -0,0 +1,1292 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package uniffi.pubkymobile; + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the details of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.IntegerType +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.Callback +import com.sun.jna.ptr.* +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.CharBuffer +import java.nio.charset.CodingErrorAction +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + @JvmField var capacity: Int = 0 + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue: RustBuffer(), Structure.ByValue + class ByReference: RustBuffer(), Structure.ByReference + + companion object { + internal fun alloc(size: Int = 0) = rustCall() { status -> + _UniFFILib.INSTANCE.ffi_pubkymobile_rustbuffer_alloc(size, status) + }.also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + + internal fun create(capacity: Int, len: Int, data: Pointer?): RustBuffer.ByValue { + var buf = RustBuffer.ByValue() + buf.capacity = capacity + buf.len = len + buf.data = data + return buf + } + + internal fun free(buf: RustBuffer.ByValue) = rustCall() { status -> + _UniFFILib.INSTANCE.ffi_pubkymobile_rustbuffer_free(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setInt(0, value.capacity) + pointer.setInt(4, value.len) + pointer.setPointer(8, value.data) + } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getInt(0)) + value.writeField("len", pointer.getInt(4)) + value.writeField("data", pointer.getPointer(8)) + + return value + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} +// The FfiConverter interface handles converter types to and from the FFI +// +// All implementing objects should be public to support external types. When a +// type is external we need to import it's FfiConverter. +public interface FfiConverter { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): Int + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity.toLong()).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +// FfiConverter that uses `RustBuffer` as the FfiType +public interface FfiConverterRustBuffer: FfiConverter { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. +// Error runtime. +@Structure.FieldOrder("code", "error_buf") +internal open class RustCallStatus : Structure() { + @JvmField var code: Byte = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + class ByValue: RustCallStatus(), Structure.ByValue + + fun isSuccess(): Boolean { + return code == 0.toByte() + } + + fun isError(): Boolean { + return code == 1.toByte() + } + + fun isPanic(): Boolean { + return code == 2.toByte() + } +} + +class InternalException(message: String) : Exception(message) + +// Each top-level error class has a companion object that can lift the error from the call status's rust buffer +interface CallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun rustCallWithError(errorHandler: CallStatusErrorHandler, callback: (RustCallStatus) -> U): U { + var status = RustCallStatus(); + val return_value = callback(status) + checkCallStatus(errorHandler, status) + return return_value +} + +// Check RustCallStatus and throw an error if the call wasn't successful +private fun checkCallStatus(errorHandler: CallStatusErrorHandler, status: RustCallStatus) { + if (status.isSuccess()) { + return + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException(FfiConverterString.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +// CallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR +object NullCallStatusErrorHandler: CallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun rustCall(callback: (RustCallStatus) -> U): U { + return rustCallWithError(NullCallStatusErrorHandler, callback); +} + +// IntegerType that matches Rust's `usize` / C's `size_t` +public class USize(value: Long = 0) : IntegerType(Native.SIZE_T_SIZE, value, true) { + // This is needed to fill in the gaps of IntegerType's implementation of Number for Kotlin. + override fun toByte() = toInt().toByte() + // Needed until https://youtrack.jetbrains.com/issue/KT-47902 is fixed. + @Deprecated("`toInt().toChar()` is deprecated") + override fun toChar() = toInt().toChar() + override fun toShort() = toInt().toShort() + + fun writeToBuffer(buf: ByteBuffer) { + // Make sure we always write usize integers using native byte-order, since they may be + // casted to pointer values + buf.order(ByteOrder.nativeOrder()) + try { + when (Native.SIZE_T_SIZE) { + 4 -> buf.putInt(toInt()) + 8 -> buf.putLong(toLong()) + else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}") + } + } finally { + buf.order(ByteOrder.BIG_ENDIAN) + } + } + + companion object { + val size: Int + get() = Native.SIZE_T_SIZE + + fun readFromBuffer(buf: ByteBuffer) : USize { + // Make sure we always read usize integers using native byte-order, since they may be + // casted from pointer values + buf.order(ByteOrder.nativeOrder()) + try { + return when (Native.SIZE_T_SIZE) { + 4 -> USize(buf.getInt().toLong()) + 8 -> USize(buf.getLong()) + else -> throw RuntimeException("Invalid SIZE_T_SIZE: ${Native.SIZE_T_SIZE}") + } + } finally { + buf.order(ByteOrder.BIG_ENDIAN) + } + } + } +} + + +// Map handles to objects +// +// This is used when the Rust code expects an opaque pointer to represent some foreign object. +// Normally we would pass a pointer to the object, but JNA doesn't support getting a pointer from an +// object reference , nor does it support leaking a reference to Rust. +// +// Instead, this class maps USize values to objects so that we can pass a pointer-sized type to +// Rust when it needs an opaque pointer. +// +// TODO: refactor callbacks to use this class +internal class UniFfiHandleMap { + private val map = ConcurrentHashMap() + // Use AtomicInteger for our counter, since we may be on a 32-bit system. 4 billion possible + // values seems like enough. If somehow we generate 4 billion handles, then this will wrap + // around back to zero and we can assume the first handle generated will have been dropped by + // then. + private val counter = java.util.concurrent.atomic.AtomicInteger(0) + + val size: Int + get() = map.size + + fun insert(obj: T): USize { + val handle = USize(counter.getAndAdd(1).toLong()) + map.put(handle, obj) + return handle + } + + fun get(handle: USize): T? { + return map.get(handle) + } + + fun remove(handle: USize): T? { + return map.remove(handle) + } +} + +// FFI type for Rust future continuations +internal interface UniFffiRustFutureContinuationCallbackType : com.sun.jna.Callback { + fun callback(continuationHandle: USize, pollResult: Short); +} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "pubkymobile" +} + +private inline fun loadIndirect( + componentName: String +): Lib { + return Native.load(findLibraryName(componentName), Lib::class.java) +} + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. + +internal interface _UniFFILib : Library { + companion object { + internal val INSTANCE: _UniFFILib by lazy { + loadIndirect<_UniFFILib>(componentName = "pubkymobile") + .also { lib: _UniFFILib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + FfiConverterTypeEventListener.register(lib) + } + } + } + + fun uniffi_pubkymobile_fn_free_eventnotifier(`ptr`: Pointer,_uniffi_out_err: RustCallStatus, + ): Unit + fun uniffi_pubkymobile_fn_init_callback_eventlistener(`callbackStub`: ForeignCallback,_uniffi_out_err: RustCallStatus, + ): Unit + fun uniffi_pubkymobile_fn_func_auth(`url`: RustBuffer.ByValue,`secretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_create_recovery_file(`secretKey`: RustBuffer.ByValue,`passphrase`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_decrypt_recovery_file(`recoveryFile`: RustBuffer.ByValue,`passphrase`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_delete_file(`url`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_generate_secret_key(_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_get(`url`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_get_public_key_from_secret_key(`secretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_list(`url`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_parse_auth_url(`url`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_publish(`recordName`: RustBuffer.ByValue,`recordContent`: RustBuffer.ByValue,`secretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_publish_https(`recordName`: RustBuffer.ByValue,`target`: RustBuffer.ByValue,`secretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_put(`url`: RustBuffer.ByValue,`content`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_remove_event_listener(_uniffi_out_err: RustCallStatus, + ): Unit + fun uniffi_pubkymobile_fn_func_resolve(`publicKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_resolve_https(`publicKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_session(`pubky`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_set_event_listener(`listener`: Long,_uniffi_out_err: RustCallStatus, + ): Unit + fun uniffi_pubkymobile_fn_func_sign_in(`secretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_sign_out(`secretKey`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun uniffi_pubkymobile_fn_func_sign_up(`secretKey`: RustBuffer.ByValue,`homeserver`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun ffi_pubkymobile_rustbuffer_alloc(`size`: Int,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun ffi_pubkymobile_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun ffi_pubkymobile_rustbuffer_free(`buf`: RustBuffer.ByValue,_uniffi_out_err: RustCallStatus, + ): Unit + fun ffi_pubkymobile_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Int,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun ffi_pubkymobile_rust_future_continuation_callback_set(`callback`: UniFffiRustFutureContinuationCallbackType, + ): Unit + fun ffi_pubkymobile_rust_future_poll_u8(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_u8(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_u8(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_u8(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Byte + fun ffi_pubkymobile_rust_future_poll_i8(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_i8(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_i8(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_i8(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Byte + fun ffi_pubkymobile_rust_future_poll_u16(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_u16(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_u16(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_u16(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Short + fun ffi_pubkymobile_rust_future_poll_i16(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_i16(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_i16(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_i16(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Short + fun ffi_pubkymobile_rust_future_poll_u32(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_u32(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_u32(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_u32(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Int + fun ffi_pubkymobile_rust_future_poll_i32(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_i32(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_i32(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_i32(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Int + fun ffi_pubkymobile_rust_future_poll_u64(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_u64(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_u64(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_u64(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Long + fun ffi_pubkymobile_rust_future_poll_i64(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_i64(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_i64(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_i64(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Long + fun ffi_pubkymobile_rust_future_poll_f32(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_f32(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_f32(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_f32(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Float + fun ffi_pubkymobile_rust_future_poll_f64(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_f64(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_f64(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_f64(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Double + fun ffi_pubkymobile_rust_future_poll_pointer(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_pointer(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_pointer(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_pointer(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Pointer + fun ffi_pubkymobile_rust_future_poll_rust_buffer(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_rust_buffer(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_rust_buffer(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_rust_buffer(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): RustBuffer.ByValue + fun ffi_pubkymobile_rust_future_poll_void(`handle`: Pointer,`uniffiCallback`: USize, + ): Unit + fun ffi_pubkymobile_rust_future_cancel_void(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_free_void(`handle`: Pointer, + ): Unit + fun ffi_pubkymobile_rust_future_complete_void(`handle`: Pointer,_uniffi_out_err: RustCallStatus, + ): Unit + fun uniffi_pubkymobile_checksum_func_auth( + ): Short + fun uniffi_pubkymobile_checksum_func_create_recovery_file( + ): Short + fun uniffi_pubkymobile_checksum_func_decrypt_recovery_file( + ): Short + fun uniffi_pubkymobile_checksum_func_delete_file( + ): Short + fun uniffi_pubkymobile_checksum_func_generate_secret_key( + ): Short + fun uniffi_pubkymobile_checksum_func_get( + ): Short + fun uniffi_pubkymobile_checksum_func_get_public_key_from_secret_key( + ): Short + fun uniffi_pubkymobile_checksum_func_list( + ): Short + fun uniffi_pubkymobile_checksum_func_parse_auth_url( + ): Short + fun uniffi_pubkymobile_checksum_func_publish( + ): Short + fun uniffi_pubkymobile_checksum_func_publish_https( + ): Short + fun uniffi_pubkymobile_checksum_func_put( + ): Short + fun uniffi_pubkymobile_checksum_func_remove_event_listener( + ): Short + fun uniffi_pubkymobile_checksum_func_resolve( + ): Short + fun uniffi_pubkymobile_checksum_func_resolve_https( + ): Short + fun uniffi_pubkymobile_checksum_func_session( + ): Short + fun uniffi_pubkymobile_checksum_func_set_event_listener( + ): Short + fun uniffi_pubkymobile_checksum_func_sign_in( + ): Short + fun uniffi_pubkymobile_checksum_func_sign_out( + ): Short + fun uniffi_pubkymobile_checksum_func_sign_up( + ): Short + fun uniffi_pubkymobile_checksum_method_eventlistener_on_event_occurred( + ): Short + fun ffi_pubkymobile_uniffi_contract_version( + ): Int + +} + +private fun uniffiCheckContractApiVersion(lib: _UniFFILib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 24 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_pubkymobile_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} + +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: _UniFFILib) { + if (lib.uniffi_pubkymobile_checksum_func_auth() != 61378.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_create_recovery_file() != 55903.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_decrypt_recovery_file() != 59688.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_delete_file() != 57905.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_generate_secret_key() != 63116.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_get() != 21596.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_get_public_key_from_secret_key() != 23603.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_list() != 8522.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_parse_auth_url() != 29088.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_publish() != 20156.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_publish_https() != 14705.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_put() != 51107.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_remove_event_listener() != 6794.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_resolve() != 18303.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_resolve_https() != 34593.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_session() != 65177.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_set_event_listener() != 19468.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_sign_in() != 21006.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_sign_out() != 59116.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_func_sign_up() != 58756.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_pubkymobile_checksum_method_eventlistener_on_event_occurred() != 39865.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +// Async support + +// Public interface members begin here. + + +public object FfiConverterString: FfiConverter { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + fun toUtf8(value: String): ByteBuffer { + // Make sure we don't have invalid UTF-16, check for lone surrogates. + return Charsets.UTF_8.newEncoder().run { + onMalformedInput(CodingErrorAction.REPORT) + encode(CharBuffer.wrap(value)) + } + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteBuf = toUtf8(value) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteBuf.limit()) + rbuf.asByteBuffer()!!.put(byteBuf) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per UTF-16 code unit which will always be + // enough. + override fun allocationSize(value: String): Int { + val sizeForLength = 4 + val sizeForString = value.length * 3 + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteBuf = toUtf8(value) + buf.putInt(byteBuf.limit()) + buf.put(byteBuf) + } +} + + +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + args.filterIsInstance() + .forEach(Disposable::destroy) + } + } +} + +inline fun T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +// The base class for all UniFFI Object types. +// +// This class provides core operations for working with the Rust `Arc` pointer to +// the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// the Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each `FFIObject` instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an `FFIObject` is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an `FFIObject` instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so will +// leak the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each `FFIObject` an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// In the future we may be able to replace some of this with automatic finalization logic, such as using +// the new "Cleaner" functionaility in Java 9. The above scheme has been designed to work even if `destroy` is +// invoked by garbage-collection machinery rather than by calling code (which by the way, it's apparently also +// possible for the JVM to finalize an object while there is an in-flight call to one of its methods [1], +// so there would still be some complexity here). +// +// Sigh...all of this for want of a robust finalization mechanism. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// +abstract class FFIObject( + protected val pointer: Pointer +): Disposable, AutoCloseable { + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + open protected fun freeRustArcPtr() { + // To be overridden in subclasses. + } + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + this.freeRustArcPtr() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.pointer) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + this.freeRustArcPtr() + } + } + } +} + +public interface EventNotifierInterface { + + companion object +} + +class EventNotifier( + pointer: Pointer +) : FFIObject(pointer), EventNotifierInterface { + + /** + * Disconnect the object from the underlying Rust object. + * + * It can be called more than once, but once called, interacting with the object + * causes an `IllegalStateException`. + * + * Clients **must** call this method once done with the object, or cause a memory leak. + */ + override protected fun freeRustArcPtr() { + rustCall() { status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_free_eventnotifier(this.pointer, status) + } + } + + + + + companion object + +} + +public object FfiConverterTypeEventNotifier: FfiConverter { + override fun lower(value: EventNotifier): Pointer = value.callWithPointer { it } + + override fun lift(value: Pointer): EventNotifier { + return EventNotifier(value) + } + + override fun read(buf: ByteBuffer): EventNotifier { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: EventNotifier) = 8 + + override fun write(value: EventNotifier, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + + + +internal typealias Handle = Long +internal class ConcurrentHandleMap( + private val leftMap: MutableMap = mutableMapOf(), + private val rightMap: MutableMap = mutableMapOf() +) { + private val lock = java.util.concurrent.locks.ReentrantLock() + private val currentHandle = AtomicLong(0L) + private val stride = 1L + + fun insert(obj: T): Handle = + lock.withLock { + rightMap[obj] ?: + currentHandle.getAndAdd(stride) + .also { handle -> + leftMap[handle] = obj + rightMap[obj] = handle + } + } + + fun get(handle: Handle) = lock.withLock { + leftMap[handle] + } + + fun delete(handle: Handle) { + this.remove(handle) + } + + fun remove(handle: Handle): T? = + lock.withLock { + leftMap.remove(handle)?.let { obj -> + rightMap.remove(obj) + obj + } + } +} + +interface ForeignCallback : com.sun.jna.Callback { + public fun callback(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int +} + +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +internal const val IDX_CALLBACK_FREE = 0 +// Callback return codes +internal const val UNIFFI_CALLBACK_SUCCESS = 0 +internal const val UNIFFI_CALLBACK_ERROR = 1 +internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 + +public abstract class FfiConverterCallbackInterface( + protected val foreignCallback: ForeignCallback +): FfiConverter { + private val handleMap = ConcurrentHandleMap() + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal abstract fun register(lib: _UniFFILib) + + fun drop(handle: Handle): RustBuffer.ByValue { + return handleMap.remove(handle).let { RustBuffer.ByValue() } + } + + override fun lift(value: Handle): CallbackInterface { + return handleMap.get(value) ?: throw InternalException("No callback in handlemap; this is a Uniffi bug") + } + + override fun read(buf: ByteBuffer) = lift(buf.getLong()) + + override fun lower(value: CallbackInterface) = + handleMap.insert(value).also { + assert(handleMap.get(it) === value) { "Handle map is not returning the object we just placed there. This is a bug in the HandleMap." } + } + + override fun allocationSize(value: CallbackInterface) = 8 + + override fun write(value: CallbackInterface, buf: ByteBuffer) { + buf.putLong(lower(value)) + } +} + +// Declaration and FfiConverters for EventListener Callback Interface + +public interface EventListener { + fun `onEventOccurred`(`eventData`: String) + + companion object +} + +// The ForeignCallback that is passed to Rust. +internal class ForeignCallbackTypeEventListener : ForeignCallback { + @Suppress("TooGenericExceptionCaught") + override fun callback(handle: Handle, method: Int, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { + val cb = FfiConverterTypeEventListener.lift(handle) + return when (method) { + IDX_CALLBACK_FREE -> { + FfiConverterTypeEventListener.drop(handle) + // Successful return + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + UNIFFI_CALLBACK_SUCCESS + } + 1 -> { + // Call the method, write to outBuf and return a status code + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` for info + try { + this.`invokeOnEventOccurred`(cb, argsData, argsLen, outBuf) + } catch (e: Throwable) { + // Unexpected error + try { + // Try to serialize the error into a string + outBuf.setValue(FfiConverterString.lower(e.toString())) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + } + + else -> { + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + try { + // Try to serialize the error into a string + outBuf.setValue(FfiConverterString.lower("Invalid Callback index")) + } catch (e: Throwable) { + // If that fails, then it's time to give up and just return + } + UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + } + } + + + @Suppress("UNUSED_PARAMETER") + private fun `invokeOnEventOccurred`(kotlinCallbackInterface: EventListener, argsData: Pointer, argsLen: Int, outBuf: RustBufferByReference): Int { + val argsBuf = argsData.getByteBuffer(0, argsLen.toLong()).also { + it.order(ByteOrder.BIG_ENDIAN) + } + fun makeCall() : Int { + kotlinCallbackInterface.`onEventOccurred`( + FfiConverterString.read(argsBuf) + ) + return UNIFFI_CALLBACK_SUCCESS + } + fun makeCallAndHandleError() : Int = makeCall() + + return makeCallAndHandleError() + } + +} + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +public object FfiConverterTypeEventListener: FfiConverterCallbackInterface( + foreignCallback = ForeignCallbackTypeEventListener() +) { + override fun register(lib: _UniFFILib) { + rustCall() { status -> + lib.uniffi_pubkymobile_fn_init_callback_eventlistener(this.foreignCallback, status) + } + } +} + + + + +public object FfiConverterSequenceString: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterString.read(buf) + } + } + + override fun allocationSize(value: List): Int { + val sizeForLength = 4 + val sizeForItems = value.map { FfiConverterString.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.forEach { + FfiConverterString.write(it, buf) + } + } +} + +fun `auth`(`url`: String, `secretKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_auth(FfiConverterString.lower(`url`),FfiConverterString.lower(`secretKey`),_status) +}) +} + + +fun `createRecoveryFile`(`secretKey`: String, `passphrase`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_create_recovery_file(FfiConverterString.lower(`secretKey`),FfiConverterString.lower(`passphrase`),_status) +}) +} + + +fun `decryptRecoveryFile`(`recoveryFile`: String, `passphrase`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_decrypt_recovery_file(FfiConverterString.lower(`recoveryFile`),FfiConverterString.lower(`passphrase`),_status) +}) +} + + +fun `deleteFile`(`url`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_delete_file(FfiConverterString.lower(`url`),_status) +}) +} + + +fun `generateSecretKey`(): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_generate_secret_key(_status) +}) +} + + +fun `get`(`url`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_get(FfiConverterString.lower(`url`),_status) +}) +} + + +fun `getPublicKeyFromSecretKey`(`secretKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_get_public_key_from_secret_key(FfiConverterString.lower(`secretKey`),_status) +}) +} + + +fun `list`(`url`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_list(FfiConverterString.lower(`url`),_status) +}) +} + + +fun `parseAuthUrl`(`url`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_parse_auth_url(FfiConverterString.lower(`url`),_status) +}) +} + + +fun `publish`(`recordName`: String, `recordContent`: String, `secretKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_publish(FfiConverterString.lower(`recordName`),FfiConverterString.lower(`recordContent`),FfiConverterString.lower(`secretKey`),_status) +}) +} + + +fun `publishHttps`(`recordName`: String, `target`: String, `secretKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_publish_https(FfiConverterString.lower(`recordName`),FfiConverterString.lower(`target`),FfiConverterString.lower(`secretKey`),_status) +}) +} + + +fun `put`(`url`: String, `content`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_put(FfiConverterString.lower(`url`),FfiConverterString.lower(`content`),_status) +}) +} + + +fun `removeEventListener`() = + + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_remove_event_listener(_status) +} + + + +fun `resolve`(`publicKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_resolve(FfiConverterString.lower(`publicKey`),_status) +}) +} + + +fun `resolveHttps`(`publicKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_resolve_https(FfiConverterString.lower(`publicKey`),_status) +}) +} + + +fun `session`(`pubky`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_session(FfiConverterString.lower(`pubky`),_status) +}) +} + + +fun `setEventListener`(`listener`: EventListener) = + + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_set_event_listener(FfiConverterTypeEventListener.lower(`listener`),_status) +} + + + +fun `signIn`(`secretKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_sign_in(FfiConverterString.lower(`secretKey`),_status) +}) +} + + +fun `signOut`(`secretKey`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_sign_out(FfiConverterString.lower(`secretKey`),_status) +}) +} + + +fun `signUp`(`secretKey`: String, `homeserver`: String): List { + return FfiConverterSequenceString.lift( + rustCall() { _status -> + _UniFFILib.INSTANCE.uniffi_pubkymobile_fn_func_sign_up(FfiConverterString.lower(`secretKey`),FfiConverterString.lower(`homeserver`),_status) +}) +} + + diff --git a/bindings/ios/module.modulemap b/bindings/ios/module.modulemap new file mode 100644 index 0000000..09b533a --- /dev/null +++ b/bindings/ios/module.modulemap @@ -0,0 +1,6 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +module pubkymobileFFI { + header "pubkymobileFFI.h" + export * +} \ No newline at end of file diff --git a/bindings/ios/pubkymobile.swift b/bindings/ios/pubkymobile.swift new file mode 100644 index 0000000..8bcf22c --- /dev/null +++ b/bindings/ios/pubkymobile.swift @@ -0,0 +1,872 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +import Foundation + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport(pubkymobileFFI) +import pubkymobileFFI +#endif + +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { + try! rustCall { ffi_pubkymobile_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { ffi_pubkymobile_rustbuffer_free(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a library of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + // TODO: This copies the buffer. Can we read directly from a + // Rust buffer? + self.init(bytes: rustBuffer.data!, count: Int(rustBuffer.len)) + } +} + +// Define reader functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. +// +// With external types, one swift source file needs to be able to call the read +// method on another source file's FfiConverter, but then what visibility +// should Reader have? +// - If Reader is fileprivate, then this means the read() must also +// be fileprivate, which doesn't work with external types. +// - If Reader is internal/public, we'll get compile errors since both source +// files will try define the same type. +// +// Instead, the read() method and these helper functions input a tuple of data + +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { + (data: data, offset: 0) +} + +// Reads an integer at the current offset, in big-endian order, and advances +// the offset on success. Throws if reading the integer would move the +// offset past the end of the buffer. +fileprivate func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset...size + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = reader.data[reader.offset] + reader.offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + reader.offset = range.upperBound + return value.bigEndian +} + +// Reads an arbitrary number of bytes, to be used to read +// raw bytes, this is useful when lifting strings +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array { + let range = reader.offset..<(reader.offset+count) + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + reader.data.copyBytes(to: buffer, from: range) + }) + reader.offset = range.upperBound + return value +} + +// Reads a float at the current offset. +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) +} + +// Reads a float at the current offset. +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) +} + +// Indicates if the offset has reached the end of the buffer. +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { + return reader.offset < reader.data.count +} + +// Define writer functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. See the above discussion on Readers for details. + +fileprivate func createWriter() -> [UInt8] { + return [] +} + +fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { + writer.append(contentsOf: byteArr) +} + +// Writes an integer in big-endian order. +// +// Warning: make sure what you are trying to write +// is in the correct type! +fileprivate func writeInt(_ writer: inout [UInt8], _ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } +} + +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { + writeInt(&writer, value.bitPattern) +} + +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { + writeInt(&writer, value.bitPattern) +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous go the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType + static func write(_ value: SwiftType, into buf: inout [UInt8]) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { + public static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + + public static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { + public static func lift(_ buf: RustBuffer) throws -> SwiftType { + var reader = createReader(data: Data(rustBuffer: buf)) + let value = try read(from: &reader) + if hasRemaining(reader) { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + + public static func lower(_ value: SwiftType) -> RustBuffer { + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) + } +} +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_PANIC: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: nil) +} + +private func rustCallWithError( + _ errorHandler: @escaping (RustBuffer) throws -> Error, + _ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: errorHandler) +} + +private func makeRustCall( + _ callback: (UnsafeMutablePointer) -> T, + errorHandler: ((RustBuffer) throws -> Error)? +) throws -> T { + uniffiEnsureInitialized() + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) + return returnedVal +} + +private func uniffiCheckCallStatus( + callStatus: RustCallStatus, + errorHandler: ((RustBuffer) throws -> Error)? +) throws { + switch callStatus.code { + case CALL_SUCCESS: + return + + case CALL_ERROR: + if let errorHandler = errorHandler { + throw try errorHandler(callStatus.errorBuf) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.unexpectedRustCallError + } + + case CALL_PANIC: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + case CALL_CANCELLED: + throw CancellationError() + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} + +// Public interface members begin here. + + +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + public static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + public static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { + let len: Int32 = try readInt(&buf) + return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + } + + public static func write(_ value: String, into buf: inout [UInt8]) { + let len = Int32(value.utf8.count) + writeInt(&buf, len) + writeBytes(&buf, value.utf8) + } +} + + +public protocol EventNotifierProtocol { + +} + +public class EventNotifier: EventNotifierProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + deinit { + try! rustCall { uniffi_pubkymobile_fn_free_eventnotifier(pointer, $0) } + } + + + + + +} + +public struct FfiConverterTypeEventNotifier: FfiConverter { + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = EventNotifier + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EventNotifier { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: EventNotifier, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> EventNotifier { + return EventNotifier(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: EventNotifier) -> UnsafeMutableRawPointer { + return value.pointer + } +} + + +public func FfiConverterTypeEventNotifier_lift(_ pointer: UnsafeMutableRawPointer) throws -> EventNotifier { + return try FfiConverterTypeEventNotifier.lift(pointer) +} + +public func FfiConverterTypeEventNotifier_lower(_ value: EventNotifier) -> UnsafeMutableRawPointer { + return FfiConverterTypeEventNotifier.lower(value) +} + +fileprivate extension NSLock { + func withLock(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + +fileprivate typealias UniFFICallbackHandle = UInt64 +fileprivate class UniFFICallbackHandleMap { + private var leftMap: [UniFFICallbackHandle: T] = [:] + private var counter: [UniFFICallbackHandle: UInt64] = [:] + private var rightMap: [ObjectIdentifier: UniFFICallbackHandle] = [:] + + private let lock = NSLock() + private var currentHandle: UniFFICallbackHandle = 0 + private let stride: UniFFICallbackHandle = 1 + + func insert(obj: T) -> UniFFICallbackHandle { + lock.withLock { + let id = ObjectIdentifier(obj as AnyObject) + let handle = rightMap[id] ?? { + currentHandle += stride + let handle = currentHandle + leftMap[handle] = obj + rightMap[id] = handle + return handle + }() + counter[handle] = (counter[handle] ?? 0) + 1 + return handle + } + } + + func get(handle: UniFFICallbackHandle) -> T? { + lock.withLock { + leftMap[handle] + } + } + + func delete(handle: UniFFICallbackHandle) { + remove(handle: handle) + } + + @discardableResult + func remove(handle: UniFFICallbackHandle) -> T? { + lock.withLock { + defer { counter[handle] = (counter[handle] ?? 1) - 1 } + guard counter[handle] == 1 else { return leftMap[handle] } + let obj = leftMap.removeValue(forKey: handle) + if let obj = obj { + rightMap.removeValue(forKey: ObjectIdentifier(obj as AnyObject)) + } + return obj + } + } +} + +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +private let IDX_CALLBACK_FREE: Int32 = 0 +// Callback return codes +private let UNIFFI_CALLBACK_SUCCESS: Int32 = 0 +private let UNIFFI_CALLBACK_ERROR: Int32 = 1 +private let UNIFFI_CALLBACK_UNEXPECTED_ERROR: Int32 = 2 + +// Declaration and FfiConverters for EventListener Callback Interface + +public protocol EventListener : AnyObject { + func onEventOccurred(eventData: String) + +} + +// The ForeignCallback that is passed to Rust. +fileprivate let foreignCallbackCallbackInterfaceEventListener : ForeignCallback = + { (handle: UniFFICallbackHandle, method: Int32, argsData: UnsafePointer, argsLen: Int32, out_buf: UnsafeMutablePointer) -> Int32 in + + + func invokeOnEventOccurred(_ swiftCallbackInterface: EventListener, _ argsData: UnsafePointer, _ argsLen: Int32, _ out_buf: UnsafeMutablePointer) throws -> Int32 { + var reader = createReader(data: Data(bytes: argsData, count: Int(argsLen))) + func makeCall() throws -> Int32 { + try swiftCallbackInterface.onEventOccurred( + eventData: try FfiConverterString.read(from: &reader) + ) + return UNIFFI_CALLBACK_SUCCESS + } + return try makeCall() + } + + + switch method { + case IDX_CALLBACK_FREE: + FfiConverterCallbackInterfaceEventListener.drop(handle: handle) + // Sucessful return + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return UNIFFI_CALLBACK_SUCCESS + case 1: + let cb: EventListener + do { + cb = try FfiConverterCallbackInterfaceEventListener.lift(handle) + } catch { + out_buf.pointee = FfiConverterString.lower("EventListener: Invalid handle") + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + do { + return try invokeOnEventOccurred(cb, argsData, argsLen, out_buf) + } catch let error { + out_buf.pointee = FfiConverterString.lower(String(describing: error)) + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: + // An unexpected error happened. + // See docs of ForeignCallback in `uniffi_core/src/ffi/foreigncallbacks.rs` + return UNIFFI_CALLBACK_UNEXPECTED_ERROR + } +} + +// FfiConverter protocol for callback interfaces +fileprivate struct FfiConverterCallbackInterfaceEventListener { + private static let initCallbackOnce: () = { + // Swift ensures this initializer code will once run once, even when accessed by multiple threads. + try! rustCall { (err: UnsafeMutablePointer) in + uniffi_pubkymobile_fn_init_callback_eventlistener(foreignCallbackCallbackInterfaceEventListener, err) + } + }() + + private static func ensureCallbackinitialized() { + _ = initCallbackOnce + } + + static func drop(handle: UniFFICallbackHandle) { + handleMap.remove(handle: handle) + } + + private static var handleMap = UniFFICallbackHandleMap() +} + +extension FfiConverterCallbackInterfaceEventListener : FfiConverter { + typealias SwiftType = EventListener + // We can use Handle as the FfiType because it's a typealias to UInt64 + typealias FfiType = UniFFICallbackHandle + + public static func lift(_ handle: UniFFICallbackHandle) throws -> SwiftType { + ensureCallbackinitialized(); + guard let callback = handleMap.get(handle: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return callback + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + ensureCallbackinitialized(); + let handle: UniFFICallbackHandle = try readInt(&buf) + return try lift(handle) + } + + public static func lower(_ v: SwiftType) -> UniFFICallbackHandle { + ensureCallbackinitialized(); + return handleMap.insert(obj: v) + } + + public static func write(_ v: SwiftType, into buf: inout [UInt8]) { + ensureCallbackinitialized(); + writeInt(&buf, lower(v)) + } +} + +fileprivate struct FfiConverterSequenceString: FfiConverterRustBuffer { + typealias SwiftType = [String] + + public static func write(_ value: [String], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterString.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [String] { + let len: Int32 = try readInt(&buf) + var seq = [String]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterString.read(from: &buf)) + } + return seq + } +} + +public func auth(url: String, secretKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_auth( + FfiConverterString.lower(url), + FfiConverterString.lower(secretKey),$0) +} + ) +} + +public func createRecoveryFile(secretKey: String, passphrase: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_create_recovery_file( + FfiConverterString.lower(secretKey), + FfiConverterString.lower(passphrase),$0) +} + ) +} + +public func decryptRecoveryFile(recoveryFile: String, passphrase: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_decrypt_recovery_file( + FfiConverterString.lower(recoveryFile), + FfiConverterString.lower(passphrase),$0) +} + ) +} + +public func deleteFile(url: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_delete_file( + FfiConverterString.lower(url),$0) +} + ) +} + +public func generateSecretKey() -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_generate_secret_key($0) +} + ) +} + +public func get(url: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_get( + FfiConverterString.lower(url),$0) +} + ) +} + +public func getPublicKeyFromSecretKey(secretKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_get_public_key_from_secret_key( + FfiConverterString.lower(secretKey),$0) +} + ) +} + +public func list(url: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_list( + FfiConverterString.lower(url),$0) +} + ) +} + +public func parseAuthUrl(url: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_parse_auth_url( + FfiConverterString.lower(url),$0) +} + ) +} + +public func publish(recordName: String, recordContent: String, secretKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_publish( + FfiConverterString.lower(recordName), + FfiConverterString.lower(recordContent), + FfiConverterString.lower(secretKey),$0) +} + ) +} + +public func publishHttps(recordName: String, target: String, secretKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_publish_https( + FfiConverterString.lower(recordName), + FfiConverterString.lower(target), + FfiConverterString.lower(secretKey),$0) +} + ) +} + +public func put(url: String, content: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_put( + FfiConverterString.lower(url), + FfiConverterString.lower(content),$0) +} + ) +} + +public func removeEventListener() { + try! rustCall() { + uniffi_pubkymobile_fn_func_remove_event_listener($0) +} +} + + + +public func resolve(publicKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_resolve( + FfiConverterString.lower(publicKey),$0) +} + ) +} + +public func resolveHttps(publicKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_resolve_https( + FfiConverterString.lower(publicKey),$0) +} + ) +} + +public func session(pubky: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_session( + FfiConverterString.lower(pubky),$0) +} + ) +} + +public func setEventListener(listener: EventListener) { + try! rustCall() { + uniffi_pubkymobile_fn_func_set_event_listener( + FfiConverterCallbackInterfaceEventListener.lower(listener),$0) +} +} + + + +public func signIn(secretKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_sign_in( + FfiConverterString.lower(secretKey),$0) +} + ) +} + +public func signOut(secretKey: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_sign_out( + FfiConverterString.lower(secretKey),$0) +} + ) +} + +public func signUp(secretKey: String, homeserver: String) -> [String] { + return try! FfiConverterSequenceString.lift( + try! rustCall() { + uniffi_pubkymobile_fn_func_sign_up( + FfiConverterString.lower(secretKey), + FfiConverterString.lower(homeserver),$0) +} + ) +} + +private enum InitializationResult { + case ok + case contractVersionMismatch + case apiChecksumMismatch +} +// Use a global variables to perform the versioning checks. Swift ensures that +// the code inside is only computed once. +private var initializationResult: InitializationResult { + // Get the bindings contract version from our ComponentInterface + let bindings_contract_version = 24 + // Get the scaffolding contract version by calling the into the dylib + let scaffolding_contract_version = ffi_pubkymobile_uniffi_contract_version() + if bindings_contract_version != scaffolding_contract_version { + return InitializationResult.contractVersionMismatch + } + if (uniffi_pubkymobile_checksum_func_auth() != 61378) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_create_recovery_file() != 55903) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_decrypt_recovery_file() != 59688) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_delete_file() != 57905) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_generate_secret_key() != 63116) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_get() != 21596) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_get_public_key_from_secret_key() != 23603) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_list() != 8522) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_parse_auth_url() != 29088) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_publish() != 20156) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_publish_https() != 14705) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_put() != 51107) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_remove_event_listener() != 6794) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_resolve() != 18303) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_resolve_https() != 34593) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_session() != 65177) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_set_event_listener() != 19468) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_sign_in() != 21006) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_sign_out() != 59116) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_func_sign_up() != 58756) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_pubkymobile_checksum_method_eventlistener_on_event_occurred() != 39865) { + return InitializationResult.apiChecksumMismatch + } + + return InitializationResult.ok +} + +private func uniffiEnsureInitialized() { + switch initializationResult { + case .ok: + break + case .contractVersionMismatch: + fatalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") + case .apiChecksumMismatch: + fatalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} \ No newline at end of file diff --git a/bindings/ios/pubkymobileFFI.h b/bindings/ios/pubkymobileFFI.h new file mode 100644 index 0000000..239d115 --- /dev/null +++ b/bindings/ios/pubkymobileFFI.h @@ -0,0 +1,292 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +#pragma once + +#include +#include +#include + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4 + #ifndef UNIFFI_SHARED_HEADER_V4 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V4 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V4 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ + +typedef struct RustBuffer +{ + int32_t capacity; + int32_t len; + uint8_t *_Nullable data; +} RustBuffer; + +typedef int32_t (*ForeignCallback)(uint64_t, int32_t, const uint8_t *_Nonnull, int32_t, RustBuffer *_Nonnull); + +// Task defined in Rust that Swift executes +typedef void (*UniFfiRustTaskCallback)(const void * _Nullable, int8_t); + +// Callback to execute Rust tasks using a Swift Task +// +// Args: +// executor: ForeignExecutor lowered into a size_t value +// delay: Delay in MS +// task: UniFfiRustTaskCallback to call +// task_data: data to pass the task callback +typedef int8_t (*UniFfiForeignExecutorCallback)(size_t, uint32_t, UniFfiRustTaskCallback _Nullable, const void * _Nullable); + +typedef struct ForeignBytes +{ + int32_t len; + const uint8_t *_Nullable data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H + +// Continuation callback for UniFFI Futures +typedef void (*UniFfiRustFutureContinuation)(void * _Nonnull, int8_t); + +// Scaffolding functions +void uniffi_pubkymobile_fn_free_eventnotifier(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +void uniffi_pubkymobile_fn_init_callback_eventlistener(ForeignCallback _Nonnull callback_stub, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_auth(RustBuffer url, RustBuffer secret_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_create_recovery_file(RustBuffer secret_key, RustBuffer passphrase, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_decrypt_recovery_file(RustBuffer recovery_file, RustBuffer passphrase, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_delete_file(RustBuffer url, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_generate_secret_key(RustCallStatus *_Nonnull out_status + +); +RustBuffer uniffi_pubkymobile_fn_func_get(RustBuffer url, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_get_public_key_from_secret_key(RustBuffer secret_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_list(RustBuffer url, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_parse_auth_url(RustBuffer url, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_publish(RustBuffer record_name, RustBuffer record_content, RustBuffer secret_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_publish_https(RustBuffer record_name, RustBuffer target, RustBuffer secret_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_put(RustBuffer url, RustBuffer content, RustCallStatus *_Nonnull out_status +); +void uniffi_pubkymobile_fn_func_remove_event_listener(RustCallStatus *_Nonnull out_status + +); +RustBuffer uniffi_pubkymobile_fn_func_resolve(RustBuffer public_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_resolve_https(RustBuffer public_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_session(RustBuffer pubky, RustCallStatus *_Nonnull out_status +); +void uniffi_pubkymobile_fn_func_set_event_listener(uint64_t listener, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_sign_in(RustBuffer secret_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_sign_out(RustBuffer secret_key, RustCallStatus *_Nonnull out_status +); +RustBuffer uniffi_pubkymobile_fn_func_sign_up(RustBuffer secret_key, RustBuffer homeserver, RustCallStatus *_Nonnull out_status +); +RustBuffer ffi_pubkymobile_rustbuffer_alloc(int32_t size, RustCallStatus *_Nonnull out_status +); +RustBuffer ffi_pubkymobile_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status +); +RustBuffer ffi_pubkymobile_rustbuffer_reserve(RustBuffer buf, int32_t additional, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_continuation_callback_set(UniFfiRustFutureContinuation _Nonnull callback +); +void ffi_pubkymobile_rust_future_poll_u8(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_u8(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_u8(void* _Nonnull handle +); +uint8_t ffi_pubkymobile_rust_future_complete_u8(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_i8(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_i8(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_i8(void* _Nonnull handle +); +int8_t ffi_pubkymobile_rust_future_complete_i8(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_u16(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_u16(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_u16(void* _Nonnull handle +); +uint16_t ffi_pubkymobile_rust_future_complete_u16(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_i16(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_i16(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_i16(void* _Nonnull handle +); +int16_t ffi_pubkymobile_rust_future_complete_i16(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_u32(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_u32(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_u32(void* _Nonnull handle +); +uint32_t ffi_pubkymobile_rust_future_complete_u32(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_i32(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_i32(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_i32(void* _Nonnull handle +); +int32_t ffi_pubkymobile_rust_future_complete_i32(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_u64(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_u64(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_u64(void* _Nonnull handle +); +uint64_t ffi_pubkymobile_rust_future_complete_u64(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_i64(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_i64(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_i64(void* _Nonnull handle +); +int64_t ffi_pubkymobile_rust_future_complete_i64(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_f32(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_f32(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_f32(void* _Nonnull handle +); +float ffi_pubkymobile_rust_future_complete_f32(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_f64(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_f64(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_f64(void* _Nonnull handle +); +double ffi_pubkymobile_rust_future_complete_f64(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_pointer(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_pointer(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_pointer(void* _Nonnull handle +); +void*_Nonnull ffi_pubkymobile_rust_future_complete_pointer(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_rust_buffer(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_rust_buffer(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_rust_buffer(void* _Nonnull handle +); +RustBuffer ffi_pubkymobile_rust_future_complete_rust_buffer(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +void ffi_pubkymobile_rust_future_poll_void(void* _Nonnull handle, void* _Nonnull uniffi_callback +); +void ffi_pubkymobile_rust_future_cancel_void(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_free_void(void* _Nonnull handle +); +void ffi_pubkymobile_rust_future_complete_void(void* _Nonnull handle, RustCallStatus *_Nonnull out_status +); +uint16_t uniffi_pubkymobile_checksum_func_auth(void + +); +uint16_t uniffi_pubkymobile_checksum_func_create_recovery_file(void + +); +uint16_t uniffi_pubkymobile_checksum_func_decrypt_recovery_file(void + +); +uint16_t uniffi_pubkymobile_checksum_func_delete_file(void + +); +uint16_t uniffi_pubkymobile_checksum_func_generate_secret_key(void + +); +uint16_t uniffi_pubkymobile_checksum_func_get(void + +); +uint16_t uniffi_pubkymobile_checksum_func_get_public_key_from_secret_key(void + +); +uint16_t uniffi_pubkymobile_checksum_func_list(void + +); +uint16_t uniffi_pubkymobile_checksum_func_parse_auth_url(void + +); +uint16_t uniffi_pubkymobile_checksum_func_publish(void + +); +uint16_t uniffi_pubkymobile_checksum_func_publish_https(void + +); +uint16_t uniffi_pubkymobile_checksum_func_put(void + +); +uint16_t uniffi_pubkymobile_checksum_func_remove_event_listener(void + +); +uint16_t uniffi_pubkymobile_checksum_func_resolve(void + +); +uint16_t uniffi_pubkymobile_checksum_func_resolve_https(void + +); +uint16_t uniffi_pubkymobile_checksum_func_session(void + +); +uint16_t uniffi_pubkymobile_checksum_func_set_event_listener(void + +); +uint16_t uniffi_pubkymobile_checksum_func_sign_in(void + +); +uint16_t uniffi_pubkymobile_checksum_func_sign_out(void + +); +uint16_t uniffi_pubkymobile_checksum_func_sign_up(void + +); +uint16_t uniffi_pubkymobile_checksum_method_eventlistener_on_event_occurred(void + +); +uint32_t ffi_pubkymobile_uniffi_contract_version(void + +); + diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..733a7a0 --- /dev/null +++ b/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Save as build.sh +case "$1" in + "ios") + ./build_ios.sh + ;; + "android") + ./build_android.sh + ;; + "all") + ./build_ios.sh && ./build_android.sh + ;; + *) + echo "Usage: $0 {ios|android|all}" + exit 1 + ;; +esac \ No newline at end of file diff --git a/build_android.sh b/build_android.sh new file mode 100755 index 0000000..b220be5 --- /dev/null +++ b/build_android.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +set -e # Exit immediately if a command exits with a non-zero status. + +echo "Starting Android build process..." + +# Define output directories +BASE_DIR="./bindings/android" +JNILIBS_DIR="$BASE_DIR/jniLibs" + +# Create output directories +mkdir -p "$BASE_DIR" +mkdir -p "$JNILIBS_DIR" + +# Remove previous build +echo "Removing previous build..." +rm -rf bindings/android/ + +# Cargo Build +echo "Building Rust libraries..." +cargo build && cd pubky && cargo build && cd pubky && cargo build && cd ../ && cd pubky-common && cargo build && cd ../ && cd pubky-homeserver && cargo build && cd ../../ + +# Modify Cargo.toml +echo "Updating Cargo.toml..." +sed -i '' 's/crate_type = .*/crate_type = ["cdylib"]/' Cargo.toml + +# Build release +echo "Building release version..." +cargo build --release + +# Install cargo-ndk if not already installed +if ! command -v cargo-ndk &> /dev/null; then + echo "Installing cargo-ndk..." + cargo install cargo-ndk +fi + +# Add Android targets +echo "Adding Android targets..." +rustup target add \ + aarch64-linux-android \ + armv7-linux-androideabi \ + i686-linux-android \ + x86_64-linux-android + +# Build for all Android architectures +echo "Building for Android architectures..." +cargo ndk \ + -o "$JNILIBS_DIR" \ + --manifest-path ./Cargo.toml \ + -t armeabi-v7a \ + -t arm64-v8a \ + -t x86 \ + -t x86_64 \ + build --release + +# Generate Kotlin bindings +echo "Generating Kotlin bindings..." +LIBRARY_PATH="./target/release/libpubkymobile.dylib" + +# Check if the library file exists +if [ ! -f "$LIBRARY_PATH" ]; then + echo "Error: Library file not found at $LIBRARY_PATH" + echo "Available files in target/release:" + ls -l ./target/release/ + exit 1 +fi + +# Create a temporary directory for initial generation +TMP_DIR=$(mktemp -d) + +# Generate the bindings to temp directory first +cargo run --bin uniffi-bindgen generate \ + --library "$LIBRARY_PATH" \ + --language kotlin \ + --out-dir "$TMP_DIR" + +# Move the Kotlin file from the nested directory to the final location +echo "Moving Kotlin file to final location..." +find "$TMP_DIR" -name "pubkymobile.kt" -exec mv {} "$BASE_DIR/" \; + +# Clean up temp directory and any remaining uniffi directories +echo "Cleaning up temporary files..." +rm -rf "$TMP_DIR" +rm -rf "$BASE_DIR/uniffi" + +# Verify the file was moved correctly +if [ ! -f "$BASE_DIR/pubkymobile.kt" ]; then + echo "Error: Kotlin bindings were not moved correctly" + echo "Contents of $BASE_DIR:" + ls -la "$BASE_DIR" + exit 1 +fi + +echo "Android build process completed successfully!" \ No newline at end of file diff --git a/build_ios.sh b/build_ios.sh new file mode 100755 index 0000000..7a2f6e0 --- /dev/null +++ b/build_ios.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -e # Exit immediately if a command exits with a non-zero status. + +echo "Starting iOS build process..." + +# Remove previous builds and ensure clean state +echo "Cleaning previous builds..." +rm -rf bindings/ios/* +rm -rf ios/ + +# Create necessary directories +echo "Creating build directories..." +mkdir -p bindings/ios/ + +# Set iOS deployment target +export IPHONEOS_DEPLOYMENT_TARGET=13.4 + +# Cargo Build +echo "Building Rust libraries..." +cargo build && cd pubky && cargo build && cd pubky && cargo build && cd ../ && cd pubky-common && cargo build && cd ../ && cd pubky-homeserver && cargo build && cd ../../ + +# Modify Cargo.toml +echo "Updating Cargo.toml..." +sed -i '' 's/crate_type = .*/crate_type = ["cdylib", "staticlib"]/' Cargo.toml + +# Build release +echo "Building release version..." +cargo build --release + +# Add iOS targets +echo "Adding iOS targets..." +rustup target add aarch64-apple-ios-sim aarch64-apple-ios + +# Build for iOS simulator and device +echo "Building for iOS targets..." +cargo build --release --target=aarch64-apple-ios-sim +cargo build --release --target=aarch64-apple-ios + +# Generate Swift bindings +echo "Generating Swift bindings..." +# First, ensure any existing generated files are removed +rm -rf ./bindings/ios/pubkymobile.swift +rm -rf ./bindings/ios/pubkymobileFFI.h +rm -rf ./bindings/ios/pubkymobileFFI.modulemap +rm -rf ./bindings/ios/Headers +rm -rf ./bindings/ios/ios-arm64 +rm -rf ./bindings/ios/ios-arm64-sim + +cargo run --bin uniffi-bindgen generate \ + --library ./target/release/libpubkymobile.dylib \ + --language swift \ + --out-dir ./bindings/ios \ + || { echo "Failed to generate Swift bindings"; exit 1; } + +# Handle modulemap file +echo "Handling modulemap file..." +if [ -f bindings/ios/pubkymobileFFI.modulemap ]; then + mv bindings/ios/pubkymobileFFI.modulemap bindings/ios/module.modulemap +else + echo "Warning: modulemap file not found" +fi + +# Clean up any existing XCFramework and temporary directories +echo "Cleaning up existing XCFramework..." +rm -rf "bindings/ios/PubkyMobile.xcframework" +rm -rf "bindings/ios/Headers" +rm -rf "bindings/ios/ios-arm64" +rm -rf "bindings/ios/ios-arm64-sim" + +# Create temporary directories for each architecture +echo "Creating architecture-specific directories..." +mkdir -p "bindings/ios/ios-arm64/Headers" +mkdir -p "bindings/ios/ios-arm64-sim/Headers" + +# Copy headers to architecture-specific directories +echo "Copying headers to architecture directories..." +cp bindings/ios/pubkymobileFFI.h "bindings/ios/ios-arm64/Headers/" +cp bindings/ios/module.modulemap "bindings/ios/ios-arm64/Headers/" +cp bindings/ios/pubkymobileFFI.h "bindings/ios/ios-arm64-sim/Headers/" +cp bindings/ios/module.modulemap "bindings/ios/ios-arm64-sim/Headers/" + +# Create XCFramework +echo "Creating XCFramework..." +xcodebuild -create-xcframework \ + -library ./target/aarch64-apple-ios-sim/release/libpubkymobile.a -headers "bindings/ios/ios-arm64-sim/Headers" \ + -library ./target/aarch64-apple-ios/release/libpubkymobile.a -headers "bindings/ios/ios-arm64/Headers" \ + -output "bindings/ios/PubkyMobile.xcframework" \ + || { echo "Failed to create XCFramework"; exit 1; } + +# Clean up temporary directories +echo "Cleaning up temporary directories..." +rm -rf "bindings/ios/ios-arm64" +rm -rf "bindings/ios/ios-arm64-sim" + +echo "iOS build process completed successfully!" \ No newline at end of file diff --git a/pubky/Cargo.lock b/pubky/Cargo.lock new file mode 100644 index 0000000..a712105 --- /dev/null +++ b/pubky/Cargo.lock @@ -0,0 +1,2811 @@ +# 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 = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", + "base64 0.22.1", + "clap", + "pubky", + "pubky-common", + "rpassword", + "tokio", + "url", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "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" +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.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[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 = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d08263faac5cde2a4d52b513dadb80846023aade56fcd8fc99ba73ba8050e92" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e2d530f35b40a84124146478cd16f34225306a8441998836466a2e2961c950" + +[[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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[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 = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[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 = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf", +] + +[[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 = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[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 = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc30da4a93ff8cb98e535d595d6de42731d4719d707bc1c86f579158751a24e" +dependencies = [ + "bitflags", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3f528b053a6d700b2734eabcd0fd49cb8230647aa72958467527b0b7917114" +dependencies = [ + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + +[[package]] +name = "hermit-abi" +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" +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 = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[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", + "httpdate", + "itoa", + "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]] +name = "hyper-util" +version = "0.1.6" +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]] +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 = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" +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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lmdb-master-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57640c190703d5ccf4a86aff4aeb749b2d287a8cb1723c76b51f39d77ab53b24" +dependencies = [ + "cc", + "doxygen-rs", + "libc", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" + +[[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 = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[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 = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +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 = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" +dependencies = [ + "cobs", + "embedded-io", + "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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[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 = [ + "base64 0.22.1", + "bytes", + "js-sys", + "pkarr", + "pubky-common", + "pubky_homeserver", + "reqwest", + "thiserror", + "tokio", + "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", + "rand", + "serde", + "thiserror", +] + +[[package]] +name = "pubky_homeserver" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +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 = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[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 = "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.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[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 = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + +[[package]] +name = "thiserror" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[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", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" +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", + "tracing", +] + +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "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", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "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" +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 = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +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" +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.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +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 = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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-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 = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml new file mode 100644 index 0000000..8514809 --- /dev/null +++ b/pubky/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +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/pubky/LICENSE b/pubky/LICENSE new file mode 100644 index 0000000..a0e67c5 --- /dev/null +++ b/pubky/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/README.md b/pubky/README.md new file mode 100644 index 0000000..23197ae --- /dev/null +++ b/pubky/README.md @@ -0,0 +1,8 @@ +# Pubky + +> 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/pubky/examples/authz/3rd-party-app/.gitignore b/pubky/examples/authz/3rd-party-app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/pubky/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/pubky/examples/authz/3rd-party-app/index.html b/pubky/examples/authz/3rd-party-app/index.html new file mode 100644 index 0000000..d2c0901 --- /dev/null +++ b/pubky/examples/authz/3rd-party-app/index.html @@ -0,0 +1,34 @@ + + + + + + + Pubky Auth Demo + + + + + + + + +
+

Third Party app!

+

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

+
+ + +
+
+ + diff --git a/pubky/examples/authz/3rd-party-app/package-lock.json b/pubky/examples/authz/3rd-party-app/package-lock.json new file mode 100644 index 0000000..fe0ff66 --- /dev/null +++ b/pubky/examples/authz/3rd-party-app/package-lock.json @@ -0,0 +1,1136 @@ +{ + "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": "^0.1.16", + "lit": "^3.2.0", + "qrcode": "^1.5.4" + }, + "devDependencies": { + "vite": "^5.4.2" + } + }, + "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": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@synonymdev/pubky/-/pubky-0.1.16.tgz", + "integrity": "sha512-jtFahEUUDfrTE7vpZx6m/uB4wMEBoqpEtuUoWCf30HH8cmm0Hfrv8v0xmwaSwPfSdcZlIG8beE5XjbX+eLmLUA==" + }, + "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/pubky/examples/authz/3rd-party-app/package.json b/pubky/examples/authz/3rd-party-app/package.json new file mode 100644 index 0000000..0a99f66 --- /dev/null +++ b/pubky/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": "^0.1.16", + "lit": "^3.2.0", + "qrcode": "^1.5.4" + }, + "devDependencies": { + "vite": "^5.4.2" + } +} diff --git a/pubky/examples/authz/3rd-party-app/public/pubky.svg b/pubky/examples/authz/3rd-party-app/public/pubky.svg new file mode 100644 index 0000000..e24e21d --- /dev/null +++ b/pubky/examples/authz/3rd-party-app/public/pubky.svg @@ -0,0 +1,9 @@ + + + + diff --git a/pubky/examples/authz/3rd-party-app/src/index.css b/pubky/examples/authz/3rd-party-app/src/index.css new file mode 100644 index 0000000..809fde4 --- /dev/null +++ b/pubky/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/pubky/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/pubky/examples/authz/3rd-party-app/src/pubky-auth-widget.js new file mode 100644 index 0000000..628f316 --- /dev/null +++ b/pubky/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -0,0 +1,391 @@ +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 { + // === Config === + + /** + * 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 }, + + // === State === + + /** + * Widget's state (open or closed) + */ + open: { type: Boolean }, + /** + * Show "copied to clipboard" note + */ + showCopied: { type: Boolean }, + + // === Internal === + testnet: { type: Boolean }, + pubky: { type: Object } + } + } + + 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.testnet = false; + this.open = false; + + /** @type {import("@synonymdev/pubky").PubkyClient} */ + this.pubkyClient = new window.pubky.PubkyClient(); + + this.caps = this.caps || "" + } + + connectedCallback() { + super.connectedCallback() + + this._generateURL() + } + + switchTestnet() { + this.testnet = !this.testnet; + + console.debug("Switching testnet"); + + if (this.testnet) { + this.pubkyClient = window.pubky.PubkyClient.testnet() + } else { + this.pubkyClient = new window.pubky.PubkyClient(); + } + + console.debug("Pkarr Relays: " + this.pubkyClient.getPkarrRelays()) + + this._generateURL() + } + + setCapabilities(caps) { + this.caps = caps || "" + + this._generateURL(this.caps); + console.debug("Updated capabilities"); + } + + + _generateURL() { + let [url, promise] = this.pubkyClient.authRequest(this.relay || DEFAULT_HTTP_RELAY, this.caps); + + promise.then(pubky => { + this.pubky = pubky.z32(); + }).catch(e => { + console.error(e) + }) + + this.authUrl = url + + this._updateQr(); + } + + _updateQr() { + if (this.canvas) { + this._setQr(this.canvas); + } + } + + _setQr(canvas) { + this.canvas = canvas + QRCode.toCanvas(canvas, this.authUrl, { + margin: 2, + scale: 8, + + color: { + light: '#fff', + dark: '#000', + }, + }); + } + + _switchOpen() { + this.open = !this.open + setTimeout(() => { this.pubky = null }, 80) + } + + 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` +
+ +
+
+ ${this.pubky + ? this.caps.length > 0 + ? html` +

Successfully authorized:

+

${this.pubky}

+

With capabilities

+ ${this.caps.split(",").map(cap => html` +

${cap}

+ `) + } + ` + : html` +

Successfully authenticated to:

+

${this.pubky}

+ ` + : html` +

Scan or copy Pubky auth URL

+
+ +
+ + ` + } +
+
+ ` + } + + _renderWidgetContentBase() { + } + + + 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 { + width: 100%; + height: var(--header-height); + display: flex; + justify-content: center; + align-items:center; + } + + .header-content { + display: flex; + justify-content: center; + align-items: baseline; + column-gap: .5rem; + } + + #widget + .header .text { + display: none; + font-weight: bold; + font-size: 1.5rem; + } + #widget.open + .header .text { + display: block + } + + #widget.open + .header { + width: var(--full-width); + justify-content: center; + } + + #pubky-icon { + height: 1.5rem; + width: 100%; + } + + #widget.open + #pubky-icon { + width: auto; + } + + #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/pubky/examples/authz/README.md b/pubky/examples/authz/README.md new file mode 100644 index 0000000..905bda6 --- /dev/null +++ b/pubky/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/pubky/examples/authz/authenticator/Cargo.toml b/pubky/examples/authz/authenticator/Cargo.toml new file mode 100644 index 0000000..932701b --- /dev/null +++ b/pubky/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/pubky/examples/authz/authenticator/src/main.rs b/pubky/examples/authz/authenticator/src/main.rs new file mode 100644 index 0000000..410b8f5 --- /dev/null +++ b/pubky/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/pubky-common/Cargo.toml b/pubky/pubky-common/Cargo.toml new file mode 100644 index 0000000..9676fba --- /dev/null +++ b/pubky/pubky-common/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "pubky-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base32 = "0.5.0" +blake3 = "1.5.1" +ed25519-dalek = "2.1.1" +once_cell = "1.19.0" +pkarr = { workspace = true } +rand = "0.8.5" +thiserror = "1.0.60" +postcard = { version = "1.0.8", features = ["alloc"] } +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/pubky-common/src/auth.rs b/pubky/pubky-common/src/auth.rs new file mode 100644 index 0000000..866fe5e --- /dev/null +++ b/pubky/pubky-common/src/auth.rs @@ -0,0 +1,279 @@ +//! Client-server Authentication using signed timesteps + +use std::sync::{Arc, Mutex}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + capabilities::{Capabilities, Capability}, + crypto::{Keypair, PublicKey, Signature}, + namespaces::PUBKY_AUTH, + timestamp::Timestamp, +}; + +// 30 seconds +const TIME_INTERVAL: u64 = 30 * 1_000_000; + +const CURRENT_VERSION: u8 = 0; +// 45 seconds in the past or the future +const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000; + +#[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, +} + +impl AuthToken { + pub fn sign(keypair: &Keypair, capabilities: impl Into) -> Self { + let timestamp = Timestamp::now(); + + 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 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(); + + 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) + } + } + } + + // === Private Methods === + + /// Remove all tokens older than two time intervals in the past. + fn gc(&self) { + let threshold = ((Timestamp::now().into_inner() / TIME_INTERVAL) - 2).to_be_bytes(); + + let mut inner = self.seen.lock().unwrap(); + + match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) { + Ok(index) | Err(index) => { + inner.drain(0..index); + } + } + } +} + +#[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(transparent)] + Postcard(#[from] postcard::Error), + #[error("AuthToken already used")] + AlreadyUsed, +} + +#[cfg(test)] +mod tests { + use crate::{ + auth::TIMESTAMP_WINDOW, capabilities::Capability, crypto::Keypair, timestamp::Timestamp, + }; + + 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 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()); + } + + #[test] + fn expired() { + let signer = Keypair::random(); + let capabilities = Capabilities(vec![Capability::root()]); + + 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/pubky-common/src/capabilities.rs b/pubky/pubky-common/src/capabilities.rs new file mode 100644 index 0000000..7929860 --- /dev/null +++ b/pubky/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/pubky-common/src/crypto.rs b/pubky/pubky-common/src/crypto.rs new file mode 100644 index 0000000..a7adea5 --- /dev/null +++ b/pubky/pubky-common/src/crypto.rs @@ -0,0 +1,71 @@ +use crypto_secretbox::{ + aead::{Aead, AeadCore, KeyInit, OsRng}, + XSalsa20Poly1305, +}; +use rand::prelude::Rng; + +pub use pkarr::{Keypair, PublicKey}; + +pub use ed25519_dalek::Signature; + +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()) +} + +pub fn random_bytes() -> [u8; N] { + let mut rng = rand::thread_rng(); + let mut arr = [0u8; N]; + + #[allow(clippy::needless_range_loop)] + for i in 0..N { + arr[i] = rng.gen(); + } + 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/pubky-common/src/lib.rs b/pubky/pubky-common/src/lib.rs new file mode 100644 index 0000000..cfb56f2 --- /dev/null +++ b/pubky/pubky-common/src/lib.rs @@ -0,0 +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/pubky-common/src/namespaces.rs b/pubky/pubky-common/src/namespaces.rs new file mode 100644 index 0000000..6aa37cd --- /dev/null +++ b/pubky/pubky-common/src/namespaces.rs @@ -0,0 +1 @@ +pub const PUBKY_AUTH: &[u8; 10] = b"PUBKY:AUTH"; diff --git a/pubky/pubky-common/src/recovery_file.rs b/pubky/pubky-common/src/recovery_file.rs new file mode 100644 index 0000000..0a2f9b4 --- /dev/null +++ b/pubky/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/pubky-common/src/session.rs b/pubky/pubky-common/src/session.rs new file mode 100644 index 0000000..972652c --- /dev/null +++ b/pubky/pubky-common/src/session.rs @@ -0,0 +1,139 @@ +use pkarr::PublicKey; +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; + +extern crate alloc; +use alloc::vec::Vec; + +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, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Session { + version: usize, + pubky: PublicKey, + created_at: u64, + /// User specified name, defaults to the user-agent. + name: String, + user_agent: String, + capabilities: Vec, +} + +impl Session { + pub fn new(token: &AuthToken, user_agent: Option) -> Self { + Self { + version: 0, + pubky: token.pubky().to_owned(), + created_at: Timestamp::now().into_inner(), + 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 { + self.user_agent = user_agent; + + if self.name.is_empty() { + self.name.clone_from(&self.user_agent) + } + + self + } + + pub fn set_capabilities(&mut self, capabilities: Vec) -> &mut Self { + self.capabilities = capabilities; + + self + } + + // === Public Methods === + + pub fn serialize(&self) -> Vec { + to_allocvec(self).expect("Session::serialize") + } + + pub fn deserialize(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(Error::EmptyPayload); + } + + if bytes[0] > 0 { + return Err(Error::UnknownVersion); + } + + Ok(from_bytes(bytes)?) + } + + // TODO: add `can_read()`, `can_write()` and `is_root()` methods +} + +pub type Result = core::result::Result; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum Error { + #[error("Empty payload")] + EmptyPayload, + #[error("Unknown version")] + UnknownVersion, + #[error(transparent)] + Postcard(#[from] postcard::Error), +} + +#[cfg(test)] +mod tests { + use crate::crypto::Keypair; + + use super::*; + + #[test] + fn serialize() { + let keypair = Keypair::from_secret_key(&[0; 32]); + let pubky = keypair.public_key(); + + 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, 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(); + + assert_eq!(deseiralized, session) + } + + #[test] + fn deserialize() { + let result = Session::deserialize(&[]); + + assert_eq!(result, Err(Error::EmptyPayload)); + } +} diff --git a/pubky/pubky-common/src/timestamp.rs b/pubky/pubky-common/src/timestamp.rs new file mode 100644 index 0000000..c3c9846 --- /dev/null +++ b/pubky/pubky-common/src/timestamp.rs @@ -0,0 +1,280 @@ +//! Strictly monotonic unix timestamp in microseconds + +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::{ + ops::{Add, Sub}, + sync::Mutex, +}; + +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; + +pub struct TimestampFactory { + clock_id: u64, + last_time: u64, +} + +impl TimestampFactory { + pub fn new() -> Self { + Self { + clock_id: rand::thread_rng().gen::() & CLOCK_MASK, + last_time: system_time() & TIME_MASK, + } + } + + pub fn now(&mut self) -> Timestamp { + // Ensure strict monotonicity. + self.last_time = (system_time() & TIME_MASK).max(self.last_time + CLOCK_MASK + 1); + + // Add clock_id to the end of the timestamp + Timestamp(self.last_time | self.clock_id) + } +} + +impl Default for TimestampFactory { + fn default() -> Self { + Self::new() + } +} + +static DEFAULT_FACTORY: Lazy> = + Lazy::new(|| Mutex::new(TimestampFactory::default())); + +/// STrictly monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64. +/// +/// The purpose of this timestamp is to unique per "user", not globally, +/// it achieves this by: +/// 1. Override the last byte with a random `clock_id`, reducing the probability +/// of two matching timestamps across multiple machines/threads. +/// 2. Gurantee that the remaining 3 bytes are ever increasing (strictly monotonic) within +/// the same thread regardless of the wall clock value +/// +/// This timestamp is also serialized as BE bytes to remain sortable. +/// If a `utf-8` encoding is necessary, it is encoded as [base32::Alphabet::Crockford] +/// to act as a sortable Id. +/// +/// U64 of microseconds is valid for the next 500 thousand years! +#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)] +pub struct Timestamp(u64); + +impl Timestamp { + pub fn now() -> Self { + DEFAULT_FACTORY.lock().unwrap().now() + } + + /// Return big endian bytes + pub fn to_bytes(&self) -> [u8; 8] { + self.0.to_be_bytes() + } + + pub fn difference(&self, rhs: &Timestamp) -> i64 { + (self.0 as i64) - (rhs.0 as i64) + } + + pub fn into_inner(&self) -> u64 { + self.0 + } +} + +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(); + f.write_str(&base32::encode(base32::Alphabet::Crockford, &bytes)) + } +} + +impl TryFrom for Timestamp { + type Error = TimestampError; + + fn try_from(value: String) -> Result { + match base32::decode(base32::Alphabet::Crockford, &value) { + Some(vec) => { + let bytes: [u8; 8] = vec + .try_into() + .map_err(|_| TimestampError::InvalidEncoding)?; + + Ok(bytes.into()) + } + None => Err(TimestampError::InvalidEncoding), + } + } +} + +impl TryFrom<&[u8]> for Timestamp { + type Error = TimestampError; + + fn try_from(bytes: &[u8]) -> Result { + let bytes: [u8; 8] = bytes + .try_into() + .map_err(|_| TimestampError::InvalidBytesLength(bytes.len()))?; + + Ok(bytes.into()) + } +} + +impl From<&Timestamp> for [u8; 8] { + fn from(timestamp: &Timestamp) -> Self { + timestamp.0.to_be_bytes() + } +} + +impl From<[u8; 8]> for Timestamp { + fn from(bytes: [u8; 8]) -> Self { + Self(u64::from_be_bytes(bytes)) + } +} + +// === U64 conversion === + +impl From for u64 { + fn from(value: Timestamp) -> Self { + value.into_inner() + } +} + +impl Add for &Timestamp { + type Output = Timestamp; + + fn add(self, rhs: u64) -> Self::Output { + Timestamp(self.0 + rhs) + } +} + +impl Sub for &Timestamp { + type Output = Timestamp; + + fn sub(self, rhs: u64) -> Self::Output { + Timestamp(self.0 - rhs) + } +} + +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 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("time drift") + .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}")] + InvalidBytesLength(usize), + #[error("Invalid timestamp encoding")] + InvalidEncoding, +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + + #[test] + fn strictly_monotonic() { + const COUNT: usize = 100; + + let mut set = HashSet::with_capacity(COUNT); + let mut vec = Vec::with_capacity(COUNT); + + for _ in 0..COUNT { + let timestamp = Timestamp::now(); + + set.insert(timestamp.clone()); + vec.push(timestamp); + } + + let mut ordered = vec.clone(); + ordered.sort(); + + assert_eq!(set.len(), COUNT, "unique"); + assert_eq!(ordered, vec, "ordered"); + } + + #[test] + fn strings() { + const COUNT: usize = 100; + + let mut set = HashSet::with_capacity(COUNT); + let mut vec = Vec::with_capacity(COUNT); + + for _ in 0..COUNT { + let string = Timestamp::now().to_string(); + + set.insert(string.clone()); + vec.push(string) + } + + let mut ordered = vec.clone(); + ordered.sort(); + + assert_eq!(set.len(), COUNT, "unique"); + assert_eq!(ordered, vec, "ordered"); + } + + #[test] + fn to_from_string() { + let timestamp = Timestamp::now(); + let string = timestamp.to_string(); + let decoded: Timestamp = string.try_into().unwrap(); + + 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/pubky-homeserver/Cargo.toml b/pubky/pubky-homeserver/Cargo.toml new file mode 100644 index 0000000..c8abfd5 --- /dev/null +++ b/pubky/pubky-homeserver/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pubky_homeserver" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.82" +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.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" +hex = "0.4.3" +pkarr = { workspace = true } +postcard = { version = "1.0.8", features = ["alloc"] } +pubky-common = { version = "0.1.0", path = "../pubky-common" } +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" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/pubky/pubky-homeserver/README.md b/pubky/pubky-homeserver/README.md new file mode 100644 index 0000000..d1799a2 --- /dev/null +++ b/pubky/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/pubky-homeserver/src/config.rs b/pubky/pubky-homeserver/src/config.rs new file mode 100644 index 0000000..f182a52 --- /dev/null +++ b/pubky/pubky-homeserver/src/config.rs @@ -0,0 +1,248 @@ +//! Configuration for the server + +use anyhow::{anyhow, Context, Result}; +use pkarr::Keypair; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::Debug, + path::{Path, PathBuf}, + time::Duration, +}; +use tracing::info; + +use pubky_common::timestamp::Timestamp; + +const DEFAULT_HOMESERVER_PORT: u16 = 6287; +const DEFAULT_STORAGE_DIR: &str = "pubky"; + +pub const DEFAULT_LIST_LIMIT: u16 = 100; +pub const DEFAULT_MAX_LIST_LIMIT: u16 = 1000; + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +struct ConfigToml { + testnet: Option, + port: Option, + bootstrap: Option>, + domain: Option, + storage: Option, + secret_key: Option, + dht_request_timeout: Option, + default_list_limit: Option, + max_list_limit: Option, +} + +/// Server configuration +#[derive(Debug, Clone)] +pub struct Config { + /// Whether or not this server is running in a testnet. + testnet: bool, + /// The configured port for this server. + port: Option, + /// Bootstrapping DHT nodes. + /// + /// Helpful to run the server locally or in testnet. + bootstrap: Option>, + /// A public domain for this server + /// necessary for web browsers running in https environment. + domain: Option, + /// Path to the storage directory. + /// + /// Defaults to a directory in the OS data directory + storage: PathBuf, + /// Server keypair. + /// + /// Defaults to a random keypair. + keypair: Keypair, + dht_request_timeout: Option, + /// The default limit of a list api if no `limit` query parameter is provided. + /// + /// Defaults to `100` + default_list_limit: u16, + /// The maximum limit of a list api, even if a `limit` query parameter is provided. + /// + /// Defaults to `1000` + max_list_limit: u16, +} + +impl Config { + fn try_from_str(value: &str) -> Result { + let config_toml: ConfigToml = toml::from_str(value)?; + + let keypair = if let Some(secret_key) = config_toml.secret_key { + let secret_key = deserialize_secret_key(secret_key)?; + Keypair::from_secret_key(&secret_key) + } else { + Keypair::random() + }; + + let storage = { + let dir = if let Some(storage) = config_toml.storage { + storage + } else { + let path = dirs_next::data_dir().ok_or_else(|| { + anyhow!("operating environment provides no directory for application data") + })?; + path.join(DEFAULT_STORAGE_DIR) + }; + + dir.join("homeserver") + }; + + let config = Config { + testnet: config_toml.testnet.unwrap_or(false), + port: config_toml.port, + bootstrap: config_toml.bootstrap, + domain: config_toml.domain, + keypair, + storage, + dht_request_timeout: config_toml.dht_request_timeout, + default_list_limit: config_toml.default_list_limit.unwrap_or(DEFAULT_LIST_LIMIT), + max_list_limit: config_toml + .default_list_limit + .unwrap_or(DEFAULT_MAX_LIST_LIMIT), + }; + + if config.testnet { + let testnet_config = Config::testnet(); + + return Ok(Config { + bootstrap: testnet_config.bootstrap, + ..config + }); + } + + 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()))?; + + Config::try_from_str(&s) + } + + /// 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 = 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 = std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join(DEFAULT_STORAGE_DIR); + + Self { + bootstrap, + storage, + ..Default::default() + } + } + + pub fn port(&self) -> u16 { + self.port.unwrap_or(DEFAULT_HOMESERVER_PORT) + } + + pub fn bootstsrap(&self) -> Option> { + self.bootstrap.to_owned() + } + + pub fn domain(&self) -> &Option { + &self.domain + } + + pub fn keypair(&self) -> &Keypair { + &self.keypair + } + + pub fn default_list_limit(&self) -> u16 { + self.default_list_limit + } + + pub fn max_list_limit(&self) -> u16 { + self.max_list_limit + } + + /// Get the path to the storage directory + pub fn storage(&self) -> &PathBuf { + &self.storage + } + + 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: None, + storage: storage(None) + .expect("operating environment provides no directory for application data"), + keypair: Keypair::random(), + dht_request_timeout: None, + default_list_limit: DEFAULT_LIST_LIMIT, + max_list_limit: DEFAULT_MAX_LIST_LIMIT, + } + } +} + +fn deserialize_secret_key(s: String) -> anyhow::Result<[u8; 32]> { + let bytes = + hex::decode(s).map_err(|_| anyhow!("secret_key in config.toml should hex encoded"))?; + + if bytes.len() != 32 { + return Err(anyhow!(format!( + "secret_key in config.toml should be 32 bytes in hex (64 characters), got: {}", + bytes.len() + ))); + } + + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + + Ok(arr) +} + +fn storage(storage: Option) -> Result { + let dir = if let Some(storage) = storage { + PathBuf::from(storage) + } else { + let path = dirs_next::data_dir().ok_or_else(|| { + anyhow!("operating environment provides no directory for application data") + })?; + path.join(DEFAULT_STORAGE_DIR) + }; + + Ok(dir.join("homeserver")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_empty() { + Config::try_from_str("").unwrap(); + } +} diff --git a/pubky/pubky-homeserver/src/config.toml b/pubky/pubky-homeserver/src/config.toml new file mode 100644 index 0000000..cb65622 --- /dev/null +++ b/pubky/pubky-homeserver/src/config.toml @@ -0,0 +1,10 @@ +# Use testnet network (local DHT) for testing. +testnet = true +# 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/pubky-homeserver/src/database.rs b/pubky/pubky-homeserver/src/database.rs new file mode 100644 index 0000000..88a3310 --- /dev/null +++ b/pubky/pubky-homeserver/src/database.rs @@ -0,0 +1,93 @@ +use std::fs; + +use heed::{Env, EnvOpenOptions}; + +mod migrations; +pub mod tables; + +use crate::config::Config; + +use tables::{Tables, TABLES_COUNT}; + +pub struct MapSize(u64); + +impl MapSize { + pub const fn new(size: u64) -> Self { + Self(size) + } + + pub fn as_usize(&self) -> usize { + self.0.try_into().unwrap_or(usize::MAX) + } +} + +pub const DEFAULT_MAP_SIZE: MapSize = MapSize::new(10_995_116_277_760); + +#[derive(Debug, Clone)] +pub struct DB { + pub(crate) env: Env, + pub(crate) tables: Tables, + pub(crate) config: Config, +} + +impl DB { + pub fn open(config: Config) -> anyhow::Result { + fs::create_dir_all(config.storage())?; + + let env = unsafe { + EnvOpenOptions::new() + .max_dbs(TABLES_COUNT) + // TODO: Add a configuration option? + .map_size(DEFAULT_MAP_SIZE.as_usize()) + .open(config.storage()) + }?; + + let tables = migrations::run(&env)?; + + let db = DB { + env, + tables, + config, + }; + + Ok(db) + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use pkarr::{mainline::Testnet, Keypair}; + + use crate::config::Config; + + use super::DB; + + #[tokio::test] + async fn entries() { + let db = DB::open(Config::test(&Testnet::new(0))).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/pubky-homeserver/src/database/migrations.rs b/pubky/pubky-homeserver/src/database/migrations.rs new file mode 100644 index 0000000..eb5a5f8 --- /dev/null +++ b/pubky/pubky-homeserver/src/database/migrations.rs @@ -0,0 +1,17 @@ +use heed::Env; + +mod m0; + +use super::tables::Tables; + +pub fn run(env: &Env) -> anyhow::Result { + let mut wtxn = env.write_txn()?; + + m0::run(env, &mut wtxn)?; + + let tables = Tables::new(env, &mut wtxn)?; + + wtxn.commit()?; + + Ok(tables) +} diff --git a/pubky/pubky-homeserver/src/database/migrations/m0.rs b/pubky/pubky-homeserver/src/database/migrations/m0.rs new file mode 100644 index 0000000..11c0e1a --- /dev/null +++ b/pubky/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/pubky-homeserver/src/database/tables.rs b/pubky/pubky-homeserver/src/database/tables.rs new file mode 100644 index 0000000..e879bd0 --- /dev/null +++ b/pubky/pubky-homeserver/src/database/tables.rs @@ -0,0 +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/pubky-homeserver/src/database/tables/blobs.rs b/pubky/pubky-homeserver/src/database/tables/blobs.rs new file mode 100644 index 0000000..c430a58 --- /dev/null +++ b/pubky/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[8..].to_vec())) + } else { + None + }; + + rtxn.commit()?; + + Ok(result) + } +} diff --git a/pubky/pubky-homeserver/src/database/tables/entries.rs b/pubky/pubky-homeserver/src/database/tables/entries.rs new file mode 100644 index 0000000..b1c7039 --- /dev/null +++ b/pubky/pubky-homeserver/src/database/tables/entries.rs @@ -0,0 +1,315 @@ +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; + +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(); + + let key = hash.as_bytes(); + + let mut bytes_with_ref_count = Vec::with_capacity(bytes.len() + 8); + bytes_with_ref_count.extend_from_slice(&u64::to_be_bytes(0)); + bytes_with_ref_count.extend_from_slice(&bytes); + + // TODO: For now, we set the first 8 bytes to a reference counter + let exists = self + .tables + .blobs + .get(&wtxn, key)? + .unwrap_or(bytes_with_ref_count.as_slice()); + + let new_count = u64::from_be_bytes(exists[0..8].try_into().unwrap()) + 1; + + bytes_with_ref_count[0..8].copy_from_slice(&u64::to_be_bytes(new_count)); + + self.tables + .blobs + .put(&mut wtxn, hash.as_bytes(), &bytes_with_ref_count)?; + + 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)?; + + let mut bytes_with_ref_count = self + .tables + .blobs + .get(&wtxn, entry.content_hash())? + .map_or(vec![], |s| s.to_vec()); + + let arr: [u8; 8] = bytes_with_ref_count[0..8].try_into().unwrap_or([0; 8]); + let reference_count = u64::from_be_bytes(arr); + + let deleted_blobs = if reference_count > 1 { + // decrement reference count + + bytes_with_ref_count[0..8].copy_from_slice(&(reference_count - 1).to_be_bytes()); + + self.tables + .blobs + .put(&mut wtxn, entry.content_hash(), &bytes_with_ref_count)?; + + true + } else { + 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 [Config::default_list_limit] and capped by [Config::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(self.config.default_list_limit()) + .min(self.config.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/pubky-homeserver/src/database/tables/events.rs b/pubky/pubky-homeserver/src/database/tables/events.rs new file mode 100644 index 0000000..76a4d46 --- /dev/null +++ b/pubky/pubky-homeserver/src/database/tables/events.rs @@ -0,0 +1,105 @@ +//! 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}; + +use crate::database::DB; + +/// 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", + } + } +} + +impl DB { + /// Returns a list of events formatted as ` `. + /// + /// - limit defaults to [Config::default_list_limit] and capped by [Config::max_list_limit] + /// - cursor is a 13 character string encoding of a timestamp + pub fn list_events( + &self, + limit: Option, + cursor: Option, + ) -> anyhow::Result> { + let txn = self.env.read_txn()?; + + let limit = limit + .unwrap_or(self.config.default_list_limit()) + .min(self.config.max_list_limit()); + + let cursor = cursor.unwrap_or("0000000000000".to_string()); + + let mut result: Vec = vec![]; + let mut next_cursor = cursor.to_string(); + + for _ in 0..limit { + match self.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(result) + } +} diff --git a/pubky/pubky-homeserver/src/database/tables/sessions.rs b/pubky/pubky-homeserver/src/database/tables/sessions.rs new file mode 100644 index 0000000..4ecd228 --- /dev/null +++ b/pubky/pubky-homeserver/src/database/tables/sessions.rs @@ -0,0 +1,51 @@ +use heed::{ + types::{Bytes, Str}, + 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/pubky-homeserver/src/database/tables/users.rs b/pubky/pubky-homeserver/src/database/tables/users.rs new file mode 100644 index 0000000..cf9b44e --- /dev/null +++ b/pubky/pubky-homeserver/src/database/tables/users.rs @@ -0,0 +1,58 @@ +use std::borrow::Cow; + +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; + +use heed::{BoxedError, BytesDecode, BytesEncode, Database}; +use pkarr::PublicKey; + +extern crate alloc; + +/// PublicKey => User. +pub type UsersTable = Database; + +pub const USERS_TABLE: &str = "users"; + +// TODO: add more adminstration metadata like quota, invitation links, etc.. +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct User { + pub created_at: u64, +} + +impl<'a> BytesEncode<'a> for User { + type EItem = Self; + + fn bytes_encode(user: &Self::EItem) -> Result, BoxedError> { + let vec = to_allocvec(user).unwrap(); + + Ok(Cow::Owned(vec)) + } +} + +impl<'a> BytesDecode<'a> for User { + type DItem = Self; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + let user: User = from_bytes(bytes).unwrap(); + + Ok(user) + } +} + +pub struct PublicKeyCodec {} + +impl<'a> BytesEncode<'a> for PublicKeyCodec { + type EItem = PublicKey; + + fn bytes_encode(pubky: &Self::EItem) -> Result, BoxedError> { + Ok(Cow::Borrowed(pubky.as_bytes())) + } +} + +impl<'a> BytesDecode<'a> for PublicKeyCodec { + type DItem = PublicKey; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + Ok(PublicKey::try_from(bytes)?) + } +} diff --git a/pubky/pubky-homeserver/src/error.rs b/pubky/pubky-homeserver/src/error.rs new file mode 100644 index 0000000..8aa58d2 --- /dev/null +++ b/pubky/pubky-homeserver/src/error.rs @@ -0,0 +1,128 @@ +//! Server error + +use axum::{ + extract::rejection::{ExtensionRejection, PathRejection, QueryRejection}, + http::StatusCode, + response::IntoResponse, +}; +use tracing::debug; + +pub type Result = core::result::Result; + +#[derive(Debug, Clone)] +pub struct Error { + // #[serde(with = "serde_status_code")] + status: StatusCode, + detail: Option, +} + +impl Default for Error { + fn default() -> Self { + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + detail: None, + } + } +} + +impl Error { + pub fn with_status(status: StatusCode) -> Error { + Self { + status, + detail: None, + } + } + + /// Create a new [`Error`]. + pub fn new(status_code: StatusCode, message: Option) -> Error { + Self { + status: status_code, + detail: message.map(|m| m.to_string()), + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + match self.detail { + Some(detail) => (self.status, detail).into_response(), + _ => (self.status,).into_response(), + } + } +} + +impl From for Error { + fn from(error: QueryRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, error.into()) + } +} + +impl From for Error { + fn from(error: ExtensionRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, error.into()) + } +} + +impl From for Error { + fn from(error: PathRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, error.into()) + } +} + +// === Pubky specific errors === + +impl From for Error { + fn from(error: pubky_common::auth::Error) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) + } +} + +impl From for Error { + fn from(error: pkarr::Error) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) + } +} + +// === INTERNAL_SERVER_ERROR === + +impl From for Error { + fn from(error: std::io::Error) -> Self { + debug!(?error); + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: heed::Error) -> Self { + debug!(?error); + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: anyhow::Error) -> Self { + debug!(?error); + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: postcard::Error) -> Self { + debug!(?error); + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: axum::Error) -> Self { + debug!(?error); + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From> for Error { + fn from(error: flume::SendError) -> Self { + debug!(?error); + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} diff --git a/pubky/pubky-homeserver/src/extractors.rs b/pubky/pubky-homeserver/src/extractors.rs new file mode 100644 index 0000000..779ce65 --- /dev/null +++ b/pubky/pubky-homeserver/src/extractors.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; + +use axum::{ + async_trait, + extract::{FromRequestParts, Path, Query}, + http::{request::Parts, StatusCode}, + response::{IntoResponse, Response}, + RequestPartsExt, +}; + +use pkarr::PublicKey; + +use crate::error::{Error, Result}; + +#[derive(Debug)] +pub struct Pubky(PublicKey); + +impl Pubky { + pub fn public_key(&self) -> &PublicKey { + &self.0 + } +} + +#[async_trait] +impl FromRequestParts for Pubky +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)?; + + let pubky_id = params + .get("pubky") + .ok_or_else(|| (StatusCode::NOT_FOUND, "pubky param missing").into_response())?; + + let public_key = PublicKey::try_from(pubky_id.to_string()) + .map_err(Error::try_from) + .map_err(IntoResponse::into_response)?; + + // TODO: return 404 if the user doesn't exist, but exclude signups. + + 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())) + } +} + +#[derive(Debug)] +pub struct ListQueryParams { + pub limit: Option, + pub cursor: Option, + pub reverse: bool, + pub shallow: bool, +} + +#[async_trait] +impl FromRequestParts for ListQueryParams +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let params: Query> = + parts.extract().await.map_err(IntoResponse::into_response)?; + + let reverse = params.contains_key("reverse"); + let shallow = params.contains_key("shallow"); + let limit = params + .get("limit") + // Treat `limit=` as None + .and_then(|l| if l.is_empty() { None } else { Some(l) }) + .and_then(|l| l.parse::().ok()); + let cursor = params + .get("cursor") + .map(|c| c.as_str()) + // Treat `cursor=` as None + .and_then(|c| { + if c.is_empty() { + None + } else { + Some(c.to_string()) + } + }); + + Ok(ListQueryParams { + reverse, + shallow, + limit, + cursor, + }) + } +} diff --git a/pubky/pubky-homeserver/src/lib.rs b/pubky/pubky-homeserver/src/lib.rs new file mode 100644 index 0000000..4a1253b --- /dev/null +++ b/pubky/pubky-homeserver/src/lib.rs @@ -0,0 +1,9 @@ +pub mod config; +mod database; +mod error; +mod extractors; +mod pkarr; +mod routes; +mod server; + +pub use server::Homeserver; diff --git a/pubky/pubky-homeserver/src/main.rs b/pubky/pubky-homeserver/src/main.rs new file mode 100644 index 0000000..dad25df --- /dev/null +++ b/pubky/pubky-homeserver/src/main.rs @@ -0,0 +1,46 @@ +use std::path::PathBuf; + +use anyhow::Result; +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( + args.tracing_env_filter + .unwrap_or("pubky_homeserver=debug,tower_http=debug".to_string()), + ) + .init(); + + 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?; + + Ok(()) +} diff --git a/pubky/pubky-homeserver/src/pkarr.rs b/pubky/pubky-homeserver/src/pkarr.rs new file mode 100644 index 0000000..c23755e --- /dev/null +++ b/pubky/pubky-homeserver/src/pkarr.rs @@ -0,0 +1,46 @@ +//! Pkarr related task + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PkarrClientAsync, SignedPacket, +}; + +pub(crate) async fn publish_server_packet( + pkarr_client: &PkarrClientAsync, + keypair: &Keypair, + 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()?); + + // Publishing port only for localhost domain, + // assuming any other domain will point to a reverse proxy + // at the conventional ports. + if domain == "localhost" { + svcb.priority = 1; + svcb.set_port(port); + + // TODO: Add more parameteres like the signer key! + // svcb.set_param(key, value) + }; + + // TODO: announce A/AAAA records as well for Noise connections? + // Or maybe Iroh's magic socket + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "@".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + pkarr_client.publish(&signed_packet).await?; + + Ok(()) +} diff --git a/pubky/pubky-homeserver/src/routes.rs b/pubky/pubky-homeserver/src/routes.rs new file mode 100644 index 0000000..7422f20 --- /dev/null +++ b/pubky/pubky-homeserver/src/routes.rs @@ -0,0 +1,43 @@ +use axum::{ + extract::DefaultBodyLimit, + routing::{delete, get, post, put}, + Router, +}; +use tower_cookies::CookieManagerLayer; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; + +use crate::server::AppState; + +use self::pkarr::pkarr_router; + +mod auth; +mod feed; +mod pkarr; +mod public; +mod root; + +fn base(state: AppState) -> Router { + Router::new() + .route("/", get(root::handler)) + .route("/signup", post(auth::signup)) + .route("/session", post(auth::signin)) + .route("/:pubky/session", get(auth::session)) + .route("/:pubky/session", delete(auth::signout)) + .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/pubky-homeserver/src/routes/auth.rs b/pubky/pubky-homeserver/src/routes/auth.rs new file mode 100644 index 0000000..dbcffe4 --- /dev/null +++ b/pubky/pubky-homeserver/src/routes/auth.rs @@ -0,0 +1,138 @@ +use axum::{ + debug_handler, + extract::State, + http::{uri::Scheme, StatusCode, Uri}, + response::IntoResponse, +}; +use axum_extra::{headers::UserAgent, TypedHeader}; +use bytes::Bytes; +use tower_cookies::{cookie::SameSite, Cookie, Cookies}; + +use pubky_common::{crypto::random_bytes, session::Session, timestamp::Timestamp}; + +use crate::{ + database::tables::{ + sessions::{SessionsTable, SESSIONS_TABLE}, + users::User, + }, + error::{Error, Result}, + extractors::Pubky, + server::AppState, +}; + +#[debug_handler] +pub async fn signup( + State(state): State, + user_agent: Option>, + cookies: Cookies, + uri: Uri, + body: Bytes, +) -> Result { + // TODO: Verify invitation link. + // TODO: add errors in case of already axisting user. + signin(State(state), user_agent, cookies, uri, body).await +} + +pub async fn session( + State(state): State, + cookies: Cookies, + pubky: Pubky, +) -> Result { + if let Some(cookie) = cookies.get(&pubky.public_key().to_string()) { + let rtxn = state.db.env.read_txn()?; + + let sessions: SessionsTable = state + .db + .env + .open_database(&rtxn, Some(SESSIONS_TABLE))? + .expect("Session table already created"); + + if let Some(session) = sessions.get(&rtxn, cookie.value())? { + let session = session.to_owned(); + rtxn.commit()?; + + // TODO: add content-type + return Ok(session); + }; + + rtxn.commit()?; + }; + + Err(Error::with_status(StatusCode::NOT_FOUND)) +} + +pub async fn signout( + State(state): State, + cookies: Cookies, + pubky: Pubky, +) -> Result { + if let Some(cookie) = cookies.get(&pubky.public_key().to_string()) { + let mut wtxn = state.db.env.write_txn()?; + + let sessions: SessionsTable = state + .db + .env + .open_database(&wtxn, Some(SESSIONS_TABLE))? + .expect("Session table already created"); + + let _ = sessions.delete(&mut wtxn, cookie.value()); + + wtxn.commit()?; + + return Ok(()); + }; + + Err(Error::with_status(StatusCode::UNAUTHORIZED)) +} + +pub async fn signin( + State(state): State, + user_agent: Option>, + cookies: Cookies, + uri: Uri, + body: Bytes, +) -> Result { + let token = state.verifier.verify(&body)?; + + let public_key = token.pubky(); + + let mut wtxn = state.db.env.write_txn()?; + + let users = state.db.tables.users; + if let Some(existing) = users.get(&wtxn, public_key)? { + users.put(&mut wtxn, public_key, &existing)?; + } else { + users.put( + &mut wtxn, + public_key, + &User { + created_at: Timestamp::now().into_inner(), + }, + )?; + } + + let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); + + let session = Session::new(&token, user_agent.map(|ua| ua.to_string())).serialize(); + + state + .db + .tables + .sessions + .put(&mut wtxn, &session_secret, &session)?; + + let mut cookie = Cookie::new(public_key.to_string(), session_secret); + + 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); + + cookies.add(cookie); + + wtxn.commit()?; + + Ok(session) +} diff --git a/pubky/pubky-homeserver/src/routes/feed.rs b/pubky/pubky-homeserver/src/routes/feed.rs new file mode 100644 index 0000000..6271aeb --- /dev/null +++ b/pubky/pubky-homeserver/src/routes/feed.rs @@ -0,0 +1,41 @@ +use axum::{ + body::Body, + extract::State, + http::{header, Response, StatusCode}, + response::IntoResponse, +}; +use pubky_common::timestamp::{Timestamp, TimestampError}; + +use crate::{ + error::{Error, Result}, + extractors::ListQueryParams, + server::AppState, +}; + +pub async fn feed( + State(state): State, + params: ListQueryParams, +) -> Result { + if let Some(ref cursor) = params.cursor { + if let Err(timestmap_error) = Timestamp::try_from(cursor.to_string()) { + let cause = match timestmap_error { + TimestampError::InvalidEncoding => { + "Cursor should be valid base32 Crockford encoding of a timestamp" + } + TimestampError::InvalidBytesLength(size) => { + &format!("Cursor should be 13 characters long, got: {size}") + } + }; + + Err(Error::new(StatusCode::BAD_REQUEST, cause.into()))? + } + } + + let result = state.db.list_events(params.limit, params.cursor)?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/plain") + .body(Body::from(result.join("\n"))) + .unwrap()) +} diff --git a/pubky/pubky-homeserver/src/routes/pkarr.rs b/pubky/pubky-homeserver/src/routes/pkarr.rs new file mode 100644 index 0000000..9e40230 --- /dev/null +++ b/pubky/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/pubky-homeserver/src/routes/public.rs b/pubky/pubky-homeserver/src/routes/public.rs new file mode 100644 index 0000000..8c6b2b9 --- /dev/null +++ b/pubky/pubky-homeserver/src/routes/public.rs @@ -0,0 +1,174 @@ +use axum::{ + body::{Body, Bytes}, + extract::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, ListQueryParams, 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, + params: ListQueryParams, +) -> 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.reverse, + params.limit, + params.cursor, + params.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/pubky-homeserver/src/routes/root.rs b/pubky/pubky-homeserver/src/routes/root.rs new file mode 100644 index 0000000..35a9482 --- /dev/null +++ b/pubky/pubky-homeserver/src/routes/root.rs @@ -0,0 +1,5 @@ +use axum::response::IntoResponse; + +pub async fn handler() -> Result { + Ok("This a Pubky homeserver.".to_string()) +} diff --git a/pubky/pubky-homeserver/src/server.rs b/pubky/pubky-homeserver/src/server.rs new file mode 100644 index 0000000..d44d346 --- /dev/null +++ b/pubky/pubky-homeserver/src/server.rs @@ -0,0 +1,169 @@ +use std::{future::IntoFuture, net::SocketAddr}; + +use anyhow::{Error, Result}; +use pubky_common::auth::AuthVerifier; +use tokio::{net::TcpListener, signal, task::JoinSet}; +use tracing::{debug, info, warn}; + +use pkarr::{ + mainline::dht::{DhtSettings, Testnet}, + PkarrClient, PkarrClientAsync, PublicKey, Settings, +}; + +use crate::{config::Config, database::DB, pkarr::publish_server_packet}; + +#[derive(Debug)] +pub struct Homeserver { + state: AppState, + tasks: JoinSet>, +} + +#[derive(Clone, Debug)] +pub(crate) struct AppState { + pub(crate) verifier: AuthVerifier, + pub(crate) db: DB, + pub(crate) pkarr_client: PkarrClientAsync, + pub(crate) config: Config, + pub(crate) port: u16, +} + +impl Homeserver { + pub async fn start(config: Config) -> Result { + debug!(?config); + + let db = DB::open(config.clone())?; + + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: config.bootstsrap(), + request_timeout: config.dht_request_timeout(), + ..Default::default() + }, + ..Default::default() + })? + .as_async(); + + let mut tasks = JoinSet::new(); + + let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port()))).await?; + + let port = listener.local_addr()?.port(); + + let state = AppState { + verifier: AuthVerifier::default(), + db, + pkarr_client, + config: config.clone(), + port, + }; + + let app = crate::routes::create_app(state.clone()); + + // Spawn http server task + tasks.spawn( + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(shutdown_signal()) + .into_future(), + ); + + info!("Homeserver listening on http://localhost:{port}"); + + publish_server_packet( + &state.pkarr_client, + config.keypair(), + &state + .config + .domain() + .clone() + .unwrap_or("localhost".to_string()), + port, + ) + .await?; + + info!( + "Homeserver listening on pubky://{}", + config.keypair().public_key() + ); + + Ok(Self { tasks, state }) + } + + /// 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 + } + + // === Getters === + + pub fn port(&self) -> u16 { + self.state.port + } + + pub fn public_key(&self) -> PublicKey { + self.state.config.keypair().public_key() + } + + // === Public Methods === + + /// Shutdown the server and wait for all tasks to complete. + pub async fn shutdown(mut self) -> Result<()> { + self.tasks.abort_all(); + self.run_until_done().await?; + Ok(()) + } + + /// Wait for all tasks to complete. + /// + /// Runs forever unless tasks fail. + pub async fn run_until_done(mut self) -> Result<()> { + let mut final_res: Result<()> = Ok(()); + while let Some(res) = self.tasks.join_next().await { + match res { + Ok(Ok(())) => {} + Err(err) if err.is_cancelled() => {} + Ok(Err(err)) => { + warn!(?err, "task failed"); + final_res = Err(Error::from(err)); + } + Err(err) => { + warn!(?err, "task panicked"); + final_res = Err(err.into()); + } + } + } + final_res + } +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + fn graceful_shutdown() { + info!("Gracefully Shutting down.."); + } + + tokio::select! { + _ = ctrl_c => graceful_shutdown(), + _ = terminate => graceful_shutdown(), + } +} diff --git a/pubky/pubky/Cargo.toml b/pubky/pubky/Cargo.toml new file mode 100644 index 0000000..6871377 --- /dev/null +++ b/pubky/pubky/Cargo.toml @@ -0,0 +1,44 @@ +[package] +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] +thiserror = "1.0.62" +wasm-bindgen = "0.2.92" +url = "2.5.2" +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] + +[package.metadata.docs.rs] +all-features = true + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ['-g', '-O'] diff --git a/pubky/pubky/pkg/.gitignore b/pubky/pubky/pkg/.gitignore new file mode 100644 index 0000000..7355b75 --- /dev/null +++ b/pubky/pubky/pkg/.gitignore @@ -0,0 +1,6 @@ +index.cjs +browser.js +coverage +node_modules +package-lock.json +pubky* diff --git a/pubky/pubky/pkg/LICENSE b/pubky/pubky/pkg/LICENSE new file mode 100644 index 0000000..a0e67c5 --- /dev/null +++ b/pubky/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/pubky/pkg/README.md b/pubky/pubky/pkg/README.md new file mode 100644 index 0000000..81b2cf4 --- /dev/null +++ b/pubky/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 pubky = 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 [PublicKey](#publickey) on success, which you can use in `client.session(pubky)` to resolve more information about the Session. + +#### 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 Uint8Array object containing the requested data, or `undefined` if `NOT_FOUND`. + +### 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/pubky/pkg/package.json b/pubky/pubky/pkg/package.json new file mode 100644 index 0000000..be3e6df --- /dev/null +++ b/pubky/pubky/pkg/package.json @@ -0,0 +1,41 @@ +{ + "name": "@synonymdev/pubky", + "type": "module", + "description": "Pubky client", + "version": "0.1.16", + "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/pubky/pkg/test/auth.js b/pubky/pubky/pkg/test/auth.js new file mode 100644 index 0000000..fe7e559 --- /dev/null +++ b/pubky/pubky/pkg/test/auth.js @@ -0,0 +1,65 @@ +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 authedPubky = await pubkyauthResponse; + + t.is(authedPubky.z32(), pubky); + + let session = await client.session(authedPubky); + t.deepEqual(session.capabilities(), capabilities.split(',')) +}) diff --git a/pubky/pubky/pkg/test/keys.js b/pubky/pubky/pkg/test/keys.js new file mode 100644 index 0000000..d036862 --- /dev/null +++ b/pubky/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/pubky/pkg/test/public.js b/pubky/pubky/pkg/test/public.js new file mode 100644 index 0000000..ec30bb2 --- /dev/null +++ b/pubky/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/pubky/pkg/test/recovery.js b/pubky/pubky/pkg/test/recovery.js new file mode 100644 index 0000000..0c033e5 --- /dev/null +++ b/pubky/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/pubky/src/bin/bundle_pubky_npm.rs b/pubky/pubky/src/bin/bundle_pubky_npm.rs new file mode 100644 index 0000000..40e9b90 --- /dev/null +++ b/pubky/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/pubky/src/bin/patch.mjs b/pubky/pubky/src/bin/patch.mjs new file mode 100644 index 0000000..a8ed503 --- /dev/null +++ b/pubky/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/pubky/src/error.rs b/pubky/pubky/src/error.rs new file mode 100644 index 0000000..c8d80e1 --- /dev/null +++ b/pubky/pubky/src/error.rs @@ -0,0 +1,56 @@ +//! Main Crate Error + +use pkarr::dns::SimpleDnsError; + +// Alias Result to be the crate Result. +pub type Result = core::result::Result; + +#[derive(thiserror::Error, Debug)] +/// Pk common Error +pub enum Error { + /// For starter, to remove as code matures. + #[error("Generic error: {0}")] + Generic(String), + + #[error("Could not resolve endpoint for {0}")] + ResolveEndpoint(String), + + #[error("Could not convert the passed type into a Url")] + InvalidUrl, + + // === Transparent === + #[error(transparent)] + Dns(#[from] SimpleDnsError), + + #[error(transparent)] + Pkarr(#[from] pkarr::Error), + + #[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/pubky/src/lib.rs b/pubky/pubky/src/lib.rs new file mode 100644 index 0000000..2b6cf42 --- /dev/null +++ b/pubky/pubky/src/lib.rs @@ -0,0 +1,36 @@ +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 error::Error; + +#[cfg(not(target_arch = "wasm32"))] +pub use crate::shared::list_builder::ListBuilder; + +#[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/pubky/src/native.rs b/pubky/pubky/src/native.rs new file mode 100644 index 0000000..81924ba --- /dev/null +++ b/pubky/pubky/src/native.rs @@ -0,0 +1,258 @@ +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, PkarrClientAsync}; + +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() + } + + // === Getters === + + /// Returns a reference to the internal [pkarr] Client. + pub fn pkarr(&self) -> &PkarrClientAsync { + &self.pkarr + } + + // === 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/pubky/src/shared/auth.rs b/pubky/pubky/src/shared/auth.rs new file mode 100644 index 0000000..aa8cfa9 --- /dev/null +++ b/pubky/pubky/src/shared/auth.rs @@ -0,0 +1,341 @@ +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() { + self.signin_with_authtoken(&token).await?; + } + + Ok(token.pubky().clone()) + } +} + +#[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 public_key = pubkyauth_response.await.unwrap(); + + assert_eq!(&public_key, &pubky); + + // 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/pubky/src/shared/list_builder.rs b/pubky/pubky/src/shared/list_builder.rs new file mode 100644 index 0000000..0eaec77 --- /dev/null +++ b/pubky/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/pubky/src/shared/mod.rs b/pubky/pubky/src/shared/mod.rs new file mode 100644 index 0000000..67b456f --- /dev/null +++ b/pubky/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/pubky/src/shared/pkarr.rs b/pubky/pubky/src/shared/pkarr.rs new file mode 100644 index 0000000..85055ef --- /dev/null +++ b/pubky/pubky/src/shared/pkarr.rs @@ -0,0 +1,336 @@ +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 endpoint_public_key.is_some() { + let url = Url::parse(&format!( + "{}://{}", + if origin.starts_with("localhost") { + "http" + } else { + "https" + }, + origin + ))?; + + return Ok(Endpoint { 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 { + 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 { url, .. } = client + .resolve_pubky_homeserver(&pubky.public_key()) + .await + .unwrap(); + + assert_eq!(url.host_str(), Some("localhost")); + assert_eq!(url.port(), Some(server.port())); + } + } +} diff --git a/pubky/pubky/src/shared/public.rs b/pubky/pubky/src/shared/public.rs new file mode 100644 index 0000000..becf2fb --- /dev/null +++ b/pubky/pubky/src/shared/public.rs @@ -0,0 +1,824 @@ +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); + } + + #[tokio::test] + async fn dont_delete_shared_blobs() { + let testnet = Testnet::new(10); + let homeserver = Homeserver::start_test(&testnet).await.unwrap(); + let client = PubkyClient::test(&testnet); + + let homeserver_pubky = homeserver.public_key(); + + let user_1 = Keypair::random(); + let user_2 = Keypair::random(); + + client.signup(&user_1, &homeserver_pubky).await.unwrap(); + client.signup(&user_2, &homeserver_pubky).await.unwrap(); + + let user_1_id = user_1.public_key(); + let user_2_id = user_2.public_key(); + + let url_1 = format!("pubky://{user_1_id}/pub/pubky.app/file/file_1"); + let url_2 = format!("pubky://{user_2_id}/pub/pubky.app/file/file_1"); + + let file = vec![1]; + client.put(url_1.as_str(), &file).await.unwrap(); + client.put(url_2.as_str(), &file).await.unwrap(); + + // Delete file 1 + client.delete(url_1.as_str()).await.unwrap(); + + let blob = client.get(url_2.as_str()).await.unwrap().unwrap(); + + assert_eq!(blob, file); + + let feed_url = format!("http://localhost:{}/events/", homeserver.port()); + + let response = client + .request( + Method::GET, + format!("{feed_url}").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://{user_1_id}/pub/pubky.app/file/file_1",), + format!("PUT pubky://{user_2_id}/pub/pubky.app/file/file_1",), + format!("DEL pubky://{user_1_id}/pub/pubky.app/file/file_1",), + lines.last().unwrap().to_string() + ] + ) + } +} diff --git a/pubky/pubky/src/wasm.rs b/pubky/pubky/src/wasm.rs new file mode 100644 index 0000000..cbbf71b --- /dev/null +++ b/pubky/pubky/src/wasm.rs @@ -0,0 +1,250 @@ +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(JsValue::from)?, + )) + } + + /// 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(|pubky| JsValue::from(PublicKey(pubky))) + .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/pubky/src/wasm/http.rs b/pubky/pubky/src/wasm/http.rs new file mode 100644 index 0000000..61fee29 --- /dev/null +++ b/pubky/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/pubky/src/wasm/keys.rs b/pubky/pubky/src/wasm/keys.rs new file mode 100644 index 0000000..3b27045 --- /dev/null +++ b/pubky/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/pubky/src/wasm/pkarr.rs b/pubky/pubky/src/wasm/pkarr.rs new file mode 100644 index 0000000..49726f6 --- /dev/null +++ b/pubky/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/pubky/src/wasm/recovery_file.rs b/pubky/pubky/src/wasm/recovery_file.rs new file mode 100644 index 0000000..7b85178 --- /dev/null +++ b/pubky/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/pubky/src/wasm/session.rs b/pubky/pubky/src/wasm/session.rs new file mode 100644 index 0000000..e838a80 --- /dev/null +++ b/pubky/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() + } +} diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..0d27dc2 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,80 @@ +use crate::keypair::get_keypair_from_secret_key; +use crate::{PubkyAuthDetails, Capability}; +use crate::utils::create_response_vector; +use std::collections::HashMap; +use pubky::PubkyClient; +use serde_json; +use url::Url; + +pub async fn authorize(url: String, secret_key: String) -> Vec { + let client = PubkyClient::testnet(); + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + + let parsed_url = match Url::parse(&url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + + match client.send_auth_token(&keypair, parsed_url).await { + Ok(_) => create_response_vector(false, "send_auth_token success".to_string()), + Err(error) => create_response_vector(true, format!("send_auth_token failure: {}", error)), + } +} + +pub fn pubky_auth_details_to_json(details: &PubkyAuthDetails) -> Result { + serde_json::to_string(details).map_err(|_| "Error serializing to JSON".to_string()) +} + +pub fn parse_pubky_auth_url(url_str: &str) -> Result { + let url = Url::parse(url_str).map_err(|_| "Invalid URL".to_string())?; + + if url.scheme() != "pubkyauth" { + return Err("Invalid scheme, expected 'pubkyauth'".to_string()); + } + + // Collect query pairs into a HashMap for efficient access + let query_params: HashMap<_, _> = url.query_pairs().into_owned().collect(); + + let relay = query_params + .get("relay") + .cloned() + .ok_or_else(|| "Missing relay".to_string())?; + + let capabilities_str = query_params + .get("capabilities") + .or_else(|| query_params.get("caps")) + .cloned() + .ok_or_else(|| "Missing capabilities".to_string())?; + + let secret = query_params + .get("secret") + .cloned() + .ok_or_else(|| "Missing secret".to_string())?; + + // Parse capabilities + let capabilities = capabilities_str + .split(',') + .map(|capability| { + let mut parts = capability.splitn(2, ':'); + let path = parts + .next() + .ok_or_else(|| format!("Invalid capability format in '{}'", capability))?; + let permission = parts + .next() + .ok_or_else(|| format!("Invalid capability format in '{}'", capability))?; + Ok(Capability { + path: path.to_string(), + permission: permission.to_string(), + }) + }) + .collect::, String>>()?; + + Ok(PubkyAuthDetails { + relay, + capabilities, + secret, + }) +} diff --git a/src/bin/uniffi-bindgen.rs b/src/bin/uniffi-bindgen.rs new file mode 100644 index 0000000..f6cff6c --- /dev/null +++ b/src/bin/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/src/keypair.rs b/src/keypair.rs new file mode 100644 index 0000000..80db9c2 --- /dev/null +++ b/src/keypair.rs @@ -0,0 +1,34 @@ +use pkarr::Keypair; + +/** + * Get a keypair from a secret key + */ +pub fn get_keypair_from_secret_key(secret_key: &str) -> Result { + let bytes = match hex::decode(&secret_key) { + Ok(bytes) => bytes, + Err(_) => return Err("Failed to decode secret key".to_string()) + }; + + let secret_key_bytes: [u8; 32] = match bytes.try_into() { + Ok(secret_key) => secret_key, + Err(_) => { + return Err("Failed to convert secret key to 32-byte array".to_string()); + } + }; + + Ok(Keypair::from_secret_key(&secret_key_bytes)) +} + +/** + * Get the secret key from a keypair + */ +pub fn get_secret_key_from_keypair(keypair: &Keypair) -> String { + hex::encode(keypair.secret_key()) +} + +/** + * Generate a new keypair + */ +pub fn generate_keypair() -> Keypair { + Keypair::random() +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..310bb02 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,629 @@ +mod types; +mod keypair; +mod auth; +mod utils; + +pub use types::*; +pub use keypair::*; +pub use auth::*; +pub use utils::*; + +uniffi::setup_scaffolding!(); + +use std::str; +use std::collections::HashMap; +use base64::Engine; +use base64::engine::general_purpose; +use pubky::PubkyClient; +use hex; +use hex::ToHex; +use serde::Serialize; +use url::Url; +use tokio; +use pkarr::{PkarrClient, SignedPacket, Keypair, dns, PublicKey}; +use pkarr::dns::rdata::{RData, HTTPS, SVCB}; +use pkarr::dns::{Packet, ResourceRecord}; +use serde_json::json; +use utils::*; +use once_cell::sync::Lazy; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use pkarr::bytes::Bytes; +use pubky_common::session::Session; +use tokio::runtime::Runtime; +use tokio::time; + +static PUBKY_CLIENT: Lazy> = Lazy::new(|| { + Arc::new(PubkyClient::testnet()) +}); + +static TOKIO_RUNTIME: Lazy> = Lazy::new(|| { + Arc::new( + Runtime::new().expect("Failed to create Tokio runtime") + ) +}); + +// Define the EventListener trait +#[uniffi::export(callback_interface)] +pub trait EventListener: Send + Sync { + fn on_event_occurred(&self, event_data: String); +} + +#[derive(uniffi::Object)] +pub struct EventNotifier { + listener: Arc>>>, +} + +impl EventNotifier { + #[uniffi::constructor] + pub fn new() -> Self { + Self { + listener: Arc::new(Mutex::new(None)), + } + } + + pub fn set_listener(&self, listener: Box) { + let mut lock = self.listener.lock().unwrap(); + *lock = Some(listener); + } + + pub fn remove_listener(&self) { + let mut lock = self.listener.lock().unwrap(); + *lock = None; + } + + pub fn notify_event(&self, event_data: String) { + let lock = self.listener.lock().unwrap(); + if let Some(listener) = &*lock { + listener.on_event_occurred(event_data); + } + } +} + +static EVENT_NOTIFIER: Lazy> = Lazy::new(|| { + Arc::new(EventNotifier::new()) +}); + +#[uniffi::export] +pub fn set_event_listener(listener: Box) { + EVENT_NOTIFIER.as_ref().set_listener(listener); +} + +#[uniffi::export] +pub fn remove_event_listener() { + EVENT_NOTIFIER.as_ref().remove_listener(); +} + +pub fn start_internal_event_loop() { + let event_notifier = EVENT_NOTIFIER.clone(); + let runtime = TOKIO_RUNTIME.clone(); + runtime.spawn(async move { + let mut interval = time::interval(Duration::from_secs(2)); + loop { + interval.tick().await; + event_notifier.as_ref().notify_event("Internal event triggered".to_string()); + } + }); +} + +#[uniffi::export] +pub fn delete_file(url: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let parsed_url = match Url::parse(&url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + match client.delete(parsed_url).await { + Ok(_) => create_response_vector(false, "Deleted successfully".to_string()), + Err(error) => create_response_vector(true, format!("Failed to delete: {}", error)), + } + }) +} + +#[uniffi::export] +pub fn session(pubky: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let public_key = match PublicKey::try_from(pubky) { + Ok(key) => key, + Err(error) => return create_response_vector(true, format!("Invalid homeserver public key: {}", error)), + }; + let result = match client.session(&public_key).await { + Ok(session) => session, + Err(error) => return create_response_vector(true, format!("Failed to get session: {}", error)), + }; + let session: Session = match result { + Some(session) => session, + None => return create_response_vector(true, "No session returned".to_string()), + }; + + let json_obj = json!({ + "pubky": session.pubky().to_string(), + "capabilities": session.capabilities().iter().map(|c| c.to_string()).collect::>(), + }); + + let json_str = match serde_json::to_string(&json_obj) { + Ok(json) => json, + Err(e) => return create_response_vector(true, format!("Failed to serialize JSON: {}", e)), + }; + + create_response_vector(false, json_str) + }) +} + +#[uniffi::export] +pub fn generate_secret_key() -> Vec { + let keypair = generate_keypair(); + let secret_key = get_secret_key_from_keypair(&keypair); + let public_key = keypair.public_key(); + let uri = public_key.to_uri_string(); + let json_obj = json!({ + "secret_key": secret_key, + "public_key": public_key.to_string(), + "uri": uri, + }); + + let json_str = match serde_json::to_string(&json_obj) { + Ok(json) => json, + Err(e) => return create_response_vector(true, format!("Failed to serialize JSON: {}", e)), + }; + start_internal_event_loop(); + create_response_vector(false, json_str) +} + +#[uniffi::export] +pub fn get_public_key_from_secret_key(secret_key: String) -> Vec { + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + let public_key = keypair.public_key(); + let uri = public_key.to_uri_string(); + let json_obj = json!({ + "public_key": public_key.to_string(), + "uri": uri, + }); + + let json_str = match serde_json::to_string(&json_obj) { + Ok(json) => json, + Err(e) => return create_response_vector(true, format!("Failed to serialize JSON: {}", e)), + }; + create_response_vector(false, json_str) +} + +#[uniffi::export] +pub fn publish_https(record_name: String, target: String, secret_key: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + + // Create SVCB record with the target domain + let target = match target.as_str().try_into() { + Ok(target) => target, + Err(e) => return create_response_vector(true, format!("Invalid target: {}", e)), + }; + let svcb = SVCB::new(0, target); + + // Create HTTPS record + let https_record = HTTPS(svcb); + + // Create DNS packet + let mut packet = Packet::new_reply(0); + let dns_name = match dns::Name::new(&record_name) { + Ok(name) => name, + Err(e) => return create_response_vector(true, format!("Invalid DNS name: {}", e)), + }; + + packet.answers.push(ResourceRecord::new( + dns_name, + dns::CLASS::IN, + 3600, // TTL in seconds + dns::rdata::RData::HTTPS(https_record), + )); + + let signed_packet = match SignedPacket::from_packet(&keypair, &packet) { + Ok(signed_packet) => signed_packet, + Err(e) => return create_response_vector(true, format!("Failed to create signed packet: {}", e)), + }; + + match client.pkarr().publish(&signed_packet).await { + Ok(()) => create_response_vector(false, keypair.public_key().to_string()), + Err(e) => create_response_vector(true, format!("Failed to publish: {}", e)), + } + }) +} + +#[uniffi::export] +pub fn resolve_https(public_key: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let public_key = match public_key.as_str().try_into() { + Ok(key) => key, + Err(e) => return create_response_vector(true, format!("Invalid public key: {}", e)), + }; + + let client = PUBKY_CLIENT.clone(); + + match client.pkarr().resolve(&public_key).await { + Ok(Some(signed_packet)) => { + // Extract HTTPS records from the signed packet + let https_records: Vec = signed_packet.packet().answers.iter() + .filter_map(|record| { + if let dns::rdata::RData::HTTPS(https) = &record.rdata { + // Create a JSON object + let mut https_json = serde_json::json!({ + "name": record.name.to_string(), + "class": format!("{:?}", record.class), + "ttl": record.ttl, + "priority": https.0.priority, + "target": https.0.target.to_string(), + }); + + // Access specific parameters using the constants from SVCB + if let Some(port_param) = https.0.get_param(SVCB::PORT) { + if port_param.len() == 2 { + let port = u16::from_be_bytes([port_param[0], port_param[1]]); + https_json["port"] = serde_json::json!(port); + } + } + + // Access ALPN parameter if needed + if let Some(alpn_param) = https.0.get_param(SVCB::ALPN) { + // Parse ALPN protocols (list of character strings) + let mut position = 0; + let mut alpn_protocols = Vec::new(); + while position < alpn_param.len() { + let length = alpn_param[position] as usize; + position += 1; + if position + length <= alpn_param.len() { + let protocol = String::from_utf8_lossy( + &alpn_param[position..position + length], + ); + alpn_protocols.push(protocol.to_string()); + position += length; + } else { + break; // Malformed ALPN parameter + } + } + https_json["alpn"] = serde_json::json!(alpn_protocols); + } + // TODO: Add other parameters as needed. + Some(https_json) + } else { + None + } + }) + .collect(); + + if https_records.is_empty() { + return create_response_vector(true, "No HTTPS records found".to_string()); + } + + // Create JSON response + let json_obj = json!({ + "public_key": public_key.to_string(), + "https_records": https_records, + "last_seen": signed_packet.last_seen(), + "timestamp": signed_packet.timestamp(), + }); + + let json_str = match serde_json::to_string(&json_obj) { + Ok(json) => json, + Err(e) => return create_response_vector(true, format!("Failed to serialize JSON: {}", e)), + }; + + create_response_vector(false, json_str) + }, + Ok(None) => create_response_vector(true, "No signed packet found".to_string()), + Err(e) => create_response_vector(true, format!("Failed to resolve: {}", e)), + } + }) +} + +#[uniffi::export] +pub fn sign_up(secret_key: String, homeserver: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + + let homeserver_public_key = match PublicKey::try_from(homeserver) { + Ok(key) => key, + Err(error) => return create_response_vector(true, format!("Invalid homeserver public key: {}", error)), + }; + + match client.signup(&keypair, &homeserver_public_key).await { + Ok(session) => create_response_vector(false, session.pubky().to_string()), + Err(error) => create_response_vector(true, format!("signup failure: {}", error)), + } + }) +} + +#[uniffi::export] +pub fn sign_in(secret_key: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + match client.signin(&keypair).await { + Ok(_) => create_response_vector(false, "Sign in success".to_string()), + Err(error) => { + create_response_vector(true, format!("Failed to sign in: {}", error)) + } + } + }) +} + +#[uniffi::export] +pub fn sign_out(secret_key: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + match client.signout(&keypair.public_key()).await { + Ok(_) => create_response_vector(false, "Sign out success".to_string()), + Err(error) => { + create_response_vector(true, format!("Failed to sign out: {}", error)) + } + } + }) +} + +#[uniffi::export] +pub fn put(url: String, content: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let trimmed_url = url.trim_end_matches('/'); + let parsed_url = match Url::parse(&trimmed_url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + match client.put(parsed_url, &content.as_bytes()).await { + Ok(_) => create_response_vector(false, trimmed_url.to_string()), + Err(error) => { + create_response_vector(true, format!("Failed to put: {}", error)) + } + } + }) +} + +#[uniffi::export] +pub fn get(url: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let trimmed_url = url.trim_end_matches('/'); + let parsed_url = match Url::parse(&trimmed_url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + let result: Option = match client.get(parsed_url).await { + Ok(res) => res, + Err(_) => return create_response_vector(true, "Request failed".to_string()), + }; + let bytes = match result { + Some(bytes) => bytes, + None => return create_response_vector(true, "No data returned".to_string()), + }; + let string = match str::from_utf8(&bytes) { + Ok(s) => s.to_string(), + Err(_) => return create_response_vector(true, "Invalid UTF-8 sequence".to_string()), + }; + create_response_vector(false, string) + }) +} + +/** +* Resolve a signed packet from a public key +* @param public_key The public key to resolve +* @returns A vector with two elements: the first element is a boolean indicating success or failure, +* and the second element is the response data (either an error message or the resolved signed packet) +**/ +#[uniffi::export] +pub fn resolve(public_key: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let public_key = match public_key.as_str().try_into() { + Ok(key) => key, + Err(e) => return create_response_vector(true, format!("Invalid zbase32 encoded key: {}", e)), + }; + let client = PUBKY_CLIENT.clone(); + + match client.pkarr().resolve(&public_key).await { + Ok(Some(signed_packet)) => { + // Collect references to ResourceRecords from the signed packet's answers + let all_records: Vec<&ResourceRecord> = signed_packet.packet().answers.iter().collect(); + // Convert each ResourceRecord to a JSON value, handling errors appropriately + let json_records: Vec = all_records + .iter() + .filter_map(|record| { + match resource_record_to_json(record) { + Ok(json_value) => Some(json_value), + Err(e) => { + eprintln!("Error converting record to JSON: {}", e); + None + } + } + }) + .collect(); + + let bytes = signed_packet.as_bytes(); + let public_key = &bytes[..32]; + let signature = &bytes[32..96]; + let timestamp = signed_packet.timestamp(); + let dns_packet = &bytes[104..]; + let hex: String = signed_packet.encode_hex(); + + let json_obj = json!({ + "signed_packet": hex, + "public_key": general_purpose::STANDARD.encode(public_key), + "signature": general_purpose::STANDARD.encode(signature), + "timestamp": timestamp, + "last_seen": signed_packet.last_seen(), + "dns_packet": general_purpose::STANDARD.encode(dns_packet), + "records": json_records + }); + + let json_str = serde_json::to_string(&json_obj) + .expect("Failed to convert JSON object to string"); + + create_response_vector(false, json_str) + }, + Ok(None) => { + create_response_vector(true, "No signed packet found".to_string()) + } + Err(e) => { + create_response_vector(true, format!("Failed to resolve: {}", e)) + } + } + }) +} + +#[uniffi::export] +pub fn publish(record_name: String, record_content: String, secret_key: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + + let mut packet = dns::Packet::new_reply(0); + + let dns_name = match dns::Name::new(&record_name) { + Ok(name) => name, + Err(e) => return create_response_vector(true, format!("Failed to create DNS name: {}", e)), + }; + + let record_content_str: &str = record_content.as_str(); + + let txt_record = match record_content_str.try_into() { + Ok(value) => RData::TXT(value), + Err(e) => { + return create_response_vector(true, format!("Failed to convert string to TXT record: {}", e)) + } + }; + + packet.answers.push(dns::ResourceRecord::new( + dns_name, + dns::CLASS::IN, + 30, + txt_record, + )); + + match SignedPacket::from_packet(&keypair, &packet) { + Ok(signed_packet) => { + match client.pkarr().publish(&signed_packet).await { + Ok(()) => { + create_response_vector(false, keypair.public_key().to_string()) + } + Err(e) => { + create_response_vector(true, format!("Failed to publish: {}", e)) + } + } + } + Err(e) => { + create_response_vector(true, format!("Failed to create signed packet: {}", e)) + } + } + }) +} +#[uniffi::export] +pub fn list(url: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(async { + let client = PUBKY_CLIENT.clone(); + let trimmed_url = url.trim_end_matches('/'); + let parsed_url = match Url::parse(&trimmed_url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + let list_builder = match client.list(parsed_url) { + Ok(list) => list, + Err(error) => return create_response_vector(true, format!("Failed to list: {}", error)), + }; + // Execute the non-Send part synchronously + let send_future = list_builder.send(); + let send_res = match send_future.await { + Ok(res) => res, + Err(error) => return create_response_vector(true, format!("Failed to send list request: {}", error)) + }; + let json_string = match serde_json::to_string(&send_res) { + Ok(json) => json, + Err(error) => return create_response_vector(true, format!("Failed to serialize JSON: {}", error)), + }; + create_response_vector(false, json_string) + }) +} + +#[uniffi::export] +pub fn auth(url: String, secret_key: String) -> Vec { + let runtime = TOKIO_RUNTIME.clone(); + runtime.block_on(authorize(url, secret_key)) +} + +#[uniffi::export] +pub fn parse_auth_url(url: String) -> Vec { + let parsed_details = match parse_pubky_auth_url(&url) { + Ok(details) => details, + Err(error) => return create_response_vector(true, error), + }; + match pubky_auth_details_to_json(&parsed_details) { + Ok(json) => create_response_vector(false, json), + Err(error) => create_response_vector(true, error), + } +} + +#[uniffi::export] +pub fn create_recovery_file(secret_key: String, passphrase: String,) -> Vec { + if secret_key.is_empty() || passphrase.is_empty() { + return create_response_vector(true, "Secret key and passphrase must not be empty".to_string()); + } + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + let recovery_file_bytes = match PubkyClient::create_recovery_file(&keypair, &passphrase) { + Ok(bytes) => bytes, + Err(_) => return create_response_vector(true, "Failed to create recovery file".to_string()), + }; + let recovery_file = base64::encode(&recovery_file_bytes); + create_response_vector(false, recovery_file) +} + +#[uniffi::export] +pub fn decrypt_recovery_file(recovery_file: String, passphrase: String) -> Vec { + if recovery_file.is_empty() || passphrase.is_empty() { + return create_response_vector(true, "Recovery file and passphrase must not be empty".to_string()); + } + let recovery_file_bytes = match base64::decode(&recovery_file) { + Ok(bytes) => bytes, + Err(error) => return create_response_vector(true, format!("Failed to decode recovery file: {}", error)), + }; + let keypair = match PubkyClient::decrypt_recovery_file(&recovery_file_bytes, &passphrase) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, "Failed to decrypt recovery file".to_string()), + }; + let secret_key = get_secret_key_from_keypair(&keypair); + create_response_vector(false, secret_key) +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..56f9b20 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,14 @@ +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct Capability { + pub path: String, + pub permission: String, +} + +#[derive(Debug, Serialize)] +pub struct PubkyAuthDetails { + pub relay: String, + pub capabilities: Vec, + pub secret: String, +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..58833f7 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,244 @@ +use std::error::Error; +use std::net::{Ipv4Addr, Ipv6Addr}; +use serde_json::json; +use base64::{engine::general_purpose, Engine}; +use pkarr::dns::rdata::RData; +use pkarr::dns::ResourceRecord; + +pub fn create_response_vector(error: bool, data: String) -> Vec { + if error { + vec!["error".to_string(), data] + } else { + vec!["success".to_string(), data] + } +} + +pub fn extract_rdata_for_json(record: &ResourceRecord) -> serde_json::Value { + match &record.rdata { + RData::TXT(txt) => { + let attributes = txt.attributes(); + let strings: Vec = attributes.into_iter() + .map(|(key, value)| { + match value { + Some(v) => format!("{}={}", key, v), + None => key, + } + }) + .collect(); + json!({ + "type": "TXT", + "strings": strings + }) + }, + RData::A(a) => { + let ipv4 = Ipv4Addr::from(a.address); + json!({ + "type": "A", + "address": ipv4.to_string() + }) + }, + RData::AAAA(aaaa) => { + let ipv6 = Ipv6Addr::from(aaaa.address); + json!({ + "type": "AAAA", + "address": ipv6.to_string() + }) + }, + RData::AFSDB(afsdb) => { + json!({ + "type": "AFSDB", + "subtype": afsdb.subtype, + "hostname": afsdb.hostname.to_string() + }) + }, + RData::CAA(caa) => { + json!({ + "type": "CAA", + "flag": caa.flag, + "tag": caa.tag.to_string(), + "value": caa.value.to_string() + }) + }, + RData::HINFO(hinfo) => { + json!({ + "type": "HINFO", + "cpu": hinfo.cpu.to_string(), + "os": hinfo.os.to_string() + }) + }, + RData::ISDN(isdn) => { + json!({ + "type": "ISDN", + "address": isdn.address.to_string(), + "sa": isdn.sa.to_string() + }) + }, + RData::LOC(loc) => { + json!({ + "type": "LOC", + "version": loc.version, + "size": loc.size, + "horizontal_precision": loc.horizontal_precision, + "vertical_precision": loc.vertical_precision, + "latitude": loc.latitude, + "longitude": loc.longitude, + "altitude": loc.altitude + }) + }, + RData::MINFO(minfo) => { + json!({ + "type": "MINFO", + "rmailbox": minfo.rmailbox.to_string(), + "emailbox": minfo.emailbox.to_string() + }) + }, + RData::MX(mx) => { + json!({ + "type": "MX", + "preference": mx.preference, + "exchange": mx.exchange.to_string() + }) + }, + RData::NAPTR(naptr) => { + json!({ + "type": "NAPTR", + "order": naptr.order, + "preference": naptr.preference, + "flags": naptr.flags.to_string(), + "services": naptr.services.to_string(), + "regexp": naptr.regexp.to_string(), + "replacement": naptr.replacement.to_string() + }) + }, + RData::NULL(_, null_record) => { + json!({ + "type": "NULL", + "data": base64::encode(null_record.get_data()) + }) + }, + RData::OPT(opt) => { + json!({ + "type": "OPT", + "udp_packet_size": opt.udp_packet_size, + "version": opt.version, + "opt_codes": opt.opt_codes.iter().map(|code| { + json!({ + "code": code.code, + "data": base64::encode(&code.data) + }) + }).collect::>() + }) + }, + RData::RouteThrough(rt) => { + json!({ + "type": "RT", + "preference": rt.preference, + "intermediate_host": rt.intermediate_host.to_string() + }) + }, + RData::RP(rp) => { + json!({ + "type": "RP", + "mbox": rp.mbox.to_string(), + "txt": rp.txt.to_string() + }) + }, + RData::SOA(soa) => { + json!({ + "type": "SOA", + "mname": soa.mname.to_string(), + "rname": soa.rname.to_string(), + "serial": soa.serial, + "refresh": soa.refresh, + "retry": soa.retry, + "expire": soa.expire, + "minimum": soa.minimum + }) + }, + RData::SRV(srv) => { + json!({ + "type": "SRV", + "priority": srv.priority, + "weight": srv.weight, + "port": srv.port, + "target": srv.target.to_string() + }) + }, + RData::SVCB(svcb) => { + let mut params = serde_json::Map::new(); + for (key, value) in svcb.iter_params() { + params.insert(key.to_string(), json!(base64::encode(value))); + } + json!({ + "type": "SVCB", + "priority": svcb.priority, + "target": svcb.target.to_string(), + "params": params + }) + }, + RData::WKS(wks) => { + json!({ + "type": "WKS", + "address": Ipv4Addr::from(wks.address).to_string(), + "protocol": wks.protocol, + "bit_map": base64::encode(&wks.bit_map) + }) + }, + + _ => json!({ + "type": format!("{:?}", record.rdata.type_code()), + "data": "Unhandled record type" + }), + } +} + +pub fn resource_record_to_json(record: &ResourceRecord) -> Result> { + Ok(json!({ + "name": record.name.to_string(), + "class": format!("{:?}", record.class), + "ttl": record.ttl, + "rdata": extract_rdata_for_json(record), + "cache_flush": record.cache_flush + })) +} + +pub fn construct_pubky_url(public_key: &str, domain: &str, path_segments: &[&str]) -> String { + // Construct the base URL + let mut url = format!("pubky://{}/pub/{}", public_key, domain); + + // Append each path segment, separated by '/' + for segment in path_segments { + if !segment.is_empty() { + url.push('/'); + url.push_str(segment); + } + } + + // Remove trailing slash if present + if url.ends_with('/') { + url.pop(); + } + + url +} + +/** +* Extract everything up to the first instance of "pub/" in a Pubky URL +* +* # Arguments +* * `full_url` - The full URL +* +* # Returns +* * `Some(String)` - The "pub/" part of the URL +* * `None` - If "pub/" is not found in the URL +*/ +pub fn get_list_url(full_url: &str) -> Option { + if let Some(index) = full_url.find("pub/") { + let end_index = index + "pub/".len(); + let substring = &full_url[..end_index]; + Some(substring.to_string()) + } else { + // "pub/" not found in the string + None + } +} diff --git a/testing/main.rs b/testing/main.rs new file mode 100644 index 0000000..4e0ed32 --- /dev/null +++ b/testing/main.rs @@ -0,0 +1,321 @@ +use std::string::ToString; +use std::sync::Arc; +use once_cell::sync::Lazy; +use pkarr::{dns, Keypair, PublicKey, SignedPacket}; +use pkarr::bytes::Bytes; +use pkarr::dns::rdata::RData; +use pubky::PubkyClient; +use url::Url; +use std::str; + +static PUBKY_CLIENT: Lazy> = Lazy::new(|| { + // let custom_testnet = Testnet { + // bootstrap: vec!["http://localhost:6287".to_string()], + // nodes: vec![], + // }; + // + // let client = PubkyClient::builder() + // .testnet(&custom_testnet) + // .build(); + let client = PubkyClient::testnet(); + + Arc::new(client) +}); + +// static PUBKY_CLIENT: Lazy> = Lazy::new(|| { +// let custom_bootstrap = vec!["localhost:64630".to_string()]; +// +// let mut pkarr_settings = Settings::default(); +// pkarr_settings.dht.bootstrap = custom_bootstrap.clone().into(); +// pkarr_settings.resolvers = custom_bootstrap +// .iter() +// .flat_map(|resolver| resolver.to_socket_addrs()) +// .flatten() +// .collect::>() +// .into(); +// +// let client = PubkyClient::builder() +// .pkarr_settings(pkarr_settings) +// .build(); +// +// Arc::new(client) +// }); + +const HOMESERVER: &str = "pubky://8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; +const SECRET_KEY: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + +#[tokio::main] +async fn main() { + let sign_in_res = signin_or_signup(SECRET_KEY, HOMESERVER).await; + println!("Sign In/Up Response: {:?}", sign_in_res); + // let res = publish("recordname".to_string(), "recordcontent".to_string(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string()).await; + // // println!("{:?}", res); + let public_key = &sign_in_res[1]; + let url = construct_pubky_url(public_key, "mydomain.com", &[]); + let put_res = put(&url, &"newcontent".to_string()).await; + println!("Put Response: {:?}", put_res); + let get_res = get(&url).await; + println!("Get Response: {:?}", get_res); + let list_res = list(url).await; + println!("List Response: {:?}", list_res); + let create_response = create_recovery_file(&SECRET_KEY, "password"); + println!("Create Response: {:?}", create_response); + let recovery_file = create_response[1].clone(); + let decrypt_response = decrypt_recovery_file(&recovery_file, "password"); + println!("Decrypt Response: {:?}", decrypt_response); +} + +pub fn create_recovery_file(secret_key: &str, passphrase: &str,) -> Vec { + if secret_key.is_empty() || passphrase.is_empty() { + return create_response_vector(true, "Secret key and passphrase must not be empty".to_string()); + } + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + let recovery_file_bytes = match PubkyClient::create_recovery_file(&keypair, &passphrase) { + Ok(bytes) => bytes, + Err(_) => return create_response_vector(true, "Failed to create recovery file".to_string()), + }; + let recovery_file = base64::encode(&recovery_file_bytes); + create_response_vector(false, recovery_file) +} + +pub fn decrypt_recovery_file(recovery_file: &str, passphrase: &str) -> Vec { + if recovery_file.is_empty() || passphrase.is_empty() { + return create_response_vector(true, "Recovery file and passphrase must not be empty".to_string()); + } + let recovery_file_bytes = match base64::decode(&recovery_file) { + Ok(bytes) => bytes, + Err(error) => return create_response_vector(true, format!("Failed to decode recovery file: {}", error)), + }; + let keypair = match PubkyClient::decrypt_recovery_file(&recovery_file_bytes, &passphrase) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, "Failed to decrypt recovery file".to_string()), + }; + let secret_key = get_secret_key_from_keypair(&keypair); + create_response_vector(false, secret_key) +} + + +pub async fn signin_or_signup(secret_key: &str, homeserver: &str) -> Vec { + let sign_in_res = sign_in(secret_key).await; + if sign_in_res[0] == "success" { + return sign_in_res; + } + let sign_up_res = sign_up(secret_key, homeserver).await; + sign_up_res +} + +pub async fn sign_up(secret_key: &str, homeserver: &str) -> Vec { + let client = PUBKY_CLIENT.clone(); + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + + let homeserver_public_key = match PublicKey::try_from(homeserver) { + Ok(key) => key, + Err(error) => return create_response_vector(true, format!("Invalid homeserver public key: {}", error)), + }; + + match client.signup(&keypair, &homeserver_public_key).await { + Ok(session) => create_response_vector(false, session.pubky().to_string()), + Err(error) => create_response_vector(true, format!("signup failure: {}", error)), + } +} + +pub async fn sign_in(secret_key: &str) -> Vec { + let client = PUBKY_CLIENT.clone(); + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + match client.signin(&keypair).await { + Ok(session) => { + create_response_vector(false, session.pubky().to_string()) + }, + Err(error) => { + create_response_vector(true, format!("Failed to sign in: {}", error)) + } + } +} + +pub async fn publish(record_name: String, record_content: String, secret_key: String) -> Vec { + let client = PUBKY_CLIENT.clone(); + + let keypair = match get_keypair_from_secret_key(&secret_key) { + Ok(keypair) => keypair, + Err(error) => return create_response_vector(true, error), + }; + + let mut packet = dns::Packet::new_reply(0); + + let dns_name = match dns::Name::new(&record_name) { + Ok(name) => name, + Err(e) => return create_response_vector(true, format!("Failed to create DNS name: {}", e)), + }; + + let record_content_str: &str = record_content.as_str(); + + let txt_record = match record_content_str.try_into() { + Ok(value) => RData::TXT(value), + Err(e) => { + return create_response_vector(true, format!("Failed to convert string to TXT record: {}", e)) + } + }; + + packet.answers.push(dns::ResourceRecord::new( + dns_name, + dns::CLASS::IN, + 30, + txt_record, + )); + + match SignedPacket::from_packet(&keypair, &packet) { + Ok(signed_packet) => { + match client.pkarr().publish(&signed_packet).await { + Ok(()) => { + create_response_vector(false, keypair.public_key().to_string()) + } + Err(e) => { + create_response_vector(true, format!("Failed to publish: {}", e)) + } + } + } + Err(e) => { + create_response_vector(true, format!("Failed to create signed packet: {}", e)) + } + } +} + +pub fn get_keypair_from_secret_key(secret_key: &str) -> Result { + let bytes = match hex::decode(&secret_key) { + Ok(bytes) => bytes, + Err(_) => return Err("Failed to decode secret key".to_string()) + }; + + let secret_key_bytes: [u8; 32] = match bytes.try_into() { + Ok(secret_key) => secret_key, + Err(_) => { + return Err("Failed to convert secret key to 32-byte array".to_string()); + } + }; + + Ok(Keypair::from_secret_key(&secret_key_bytes)) +} + +pub fn create_response_vector(error: bool, data: String) -> Vec { + if error { + vec!["error".to_string(), data] + } else { + vec!["success".to_string(), data] + } +} + +pub async fn put(url: &String, content: &String) -> Vec { + let client = PUBKY_CLIENT.clone(); + let trimmed_url = url.trim_end_matches('/'); + let parsed_url = match Url::parse(&trimmed_url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + match client.put(parsed_url, &content.as_bytes()).await { + Ok(_) => create_response_vector(false, trimmed_url.to_string()), + Err(error) => { + create_response_vector(true, format!("Failed to put: {}", error)) + } + } +} + +pub async fn get(url: &String) -> Vec { + let client = PUBKY_CLIENT.clone(); + let trimmed_url = url.trim_end_matches('/'); + + // Parse the URL and return error early if it fails + let parsed_url = match Url::parse(&trimmed_url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + + // Perform the request and return error early if no data is returned + let result: Option = match client.get(parsed_url).await { + Ok(res) => res, + Err(_) => return create_response_vector(true, "Request failed".to_string()), + }; + + // If there are bytes, attempt to convert to UTF-8 + let bytes = match result { + Some(bytes) => bytes, + None => return create_response_vector(true, "No data returned".to_string()), + }; + + // Try to convert bytes to string and return error if it fails + let string = match str::from_utf8(&bytes) { + Ok(s) => s.to_string(), + Err(_) => return create_response_vector(true, "Invalid UTF-8 sequence".to_string()), + }; + + // If everything is successful, return the formatted response + create_response_vector(false, string) +} + +pub async fn list(url: String) -> Vec { + let client = PUBKY_CLIENT.clone(); + let trimmed_url = url.trim_end_matches('/'); + let parsed_url = match Url::parse(&trimmed_url) { + Ok(url) => url, + Err(_) => return create_response_vector(true, "Failed to parse URL".to_string()), + }; + let list_builder = match client.list(parsed_url) { + Ok(list) => list, + Err(error) => return create_response_vector(true, format!("Failed to list: {}", error)), + }; + // Execute the non-Send part synchronously + let send_future = list_builder.send(); + let send_res = match send_future.await { + Ok(res) => res, + Err(error) => return create_response_vector(true, format!("Failed to send list request: {}", error)) + }; + let json_string = match serde_json::to_string(&send_res) { + Ok(json) => json, + Err(error) => return create_response_vector(true, format!("Failed to serialize JSON: {}", error)), + }; + create_response_vector(false, json_string) +} + +fn construct_pubky_url(public_key: &str, domain: &str, path_segments: &[&str]) -> String { + // Construct the base URL + let mut url = format!("pubky://{}/pub/{}", public_key, domain); + + // Append each path segment, separated by '/' + for segment in path_segments { + if !segment.is_empty() { + url.push('/'); + url.push_str(segment); + } + } + + // Remove trailing slash if present + if url.ends_with('/') { + url.pop(); + } + + url +} + +fn get_list_url(full_url: &str) -> Option { + if let Some(index) = full_url.find("pub/") { + // Add length of "pub/" to include it in the substring + let end_index = index + "pub/".len(); + let substring = &full_url[..end_index]; + Some(substring.to_string()) + } else { + // "pub/" not found in the string + None + } +} + +pub fn get_secret_key_from_keypair(keypair: &Keypair) -> String { + hex::encode(keypair.secret_key()) +}