From dfcd5af9ef9579c0abe1825f75c11e0f3e4c318e Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 27 Nov 2025 09:55:54 +0100 Subject: [PATCH 01/21] feat: add pubky keys type --- examples/javascript/0-logging.mjs | 4 +- examples/javascript/1-testnet.mjs | 4 +- examples/javascript/2-signup.mjs | 4 +- examples/javascript/3-authenticator.mjs | 2 +- examples/javascript/README.md | 12 +- examples/rust/2-signup/README.md | 2 +- examples/rust/2-signup/signup.rs | 2 +- examples/rust/3-auth_flow/authenticator.rs | 2 +- examples/rust/5-request/README.md | 6 +- pubky-common/src/crypto.rs | 3 +- pubky-common/src/crypto/keys.rs | 198 ++++++++++++++++++ pubky-common/src/recovery_file.rs | 3 +- pubky-common/src/session.rs | 2 +- .../src/admin_server/routes/delete_entry.rs | 2 +- .../src/admin_server/routes/disable_users.rs | 2 +- pubky-homeserver/src/app_context.rs | 2 +- .../client_server/err_if_user_is_invalid.rs | 2 +- .../src/client_server/extractors.rs | 2 +- .../src/client_server/layers/authz.rs | 2 +- .../src/client_server/layers/pubky_host.rs | 2 +- .../src/client_server/routes/auth.rs | 4 +- .../src/client_server/routes/events.rs | 2 +- .../src/client_server/routes/tenants/read.rs | 9 +- .../src/client_server/routes/tenants/write.rs | 2 +- .../src/data_directory/data_dir.rs | 2 +- .../src/data_directory/mock_data_dir.rs | 10 +- .../src/data_directory/persistent_data_dir.rs | 8 +- .../data_directory/quota_config/limit_key.rs | 6 +- .../data_directory/quota_config/path_limit.rs | 2 +- pubky-homeserver/src/homeserver_app.rs | 2 +- .../persistence/files/entry/entry_layer.rs | 2 +- .../persistence/files/events/events_entity.rs | 2 +- .../persistence/files/events/events_layer.rs | 2 +- .../files/events/events_repository.rs | 2 +- .../files/events/events_service.rs | 2 +- .../persistence/files/file/file_service.rs | 14 +- .../files/opendal/opendal_service.rs | 8 +- .../src/persistence/files/user_quota_layer.rs | 43 ++-- ...0420251247_add_user_disabled_used_bytes.rs | 6 +- .../persistence/lmdb/sql_migrator/entries.rs | 2 +- .../persistence/lmdb/sql_migrator/events.rs | 2 +- .../persistence/lmdb/sql_migrator/sessions.rs | 2 +- .../lmdb/sql_migrator/signup_codes.rs | 4 +- .../persistence/lmdb/sql_migrator/users.rs | 24 ++- .../persistence/lmdb/tables/signup_tokens.rs | 2 +- .../persistence/sql/entities/entry/entity.rs | 2 +- .../sql/entities/entry/repository.rs | 12 +- .../src/persistence/sql/entities/session.rs | 4 +- .../persistence/sql/entities/signup_code.rs | 6 +- .../src/persistence/sql/entities/user.rs | 10 +- .../sql/migrations/m20250806_create_user.rs | 6 +- .../m20250812_create_signup_code.rs | 4 +- .../migrations/m20250813_create_session.rs | 4 +- .../sql/migrations/m20250814_create_event.rs | 4 +- .../sql/migrations/m20250815_create_entry.rs | 6 +- ...014_events_table_index_and_content_hash.rs | 6 +- .../src/republishers/key_republisher.rs | 4 +- .../src/republishers/user_keys_republisher.rs | 8 +- .../src/shared/pubkey_path_validator.rs | 2 +- .../src/shared/webdav/entry_path.rs | 6 +- pubky-sdk/README.md | 2 +- pubky-sdk/bindings/js/src/wrappers/keys.rs | 39 ++-- pubky-sdk/src/actors/pkdns.rs | 12 +- pubky-sdk/src/actors/session/persist.rs | 3 +- pubky-sdk/src/actors/signer/session.rs | 2 +- pubky-sdk/src/actors/storage/core.rs | 2 +- pubky-sdk/src/actors/storage/resource.rs | 42 ++-- pubky-sdk/src/client/http_targets/native.rs | 2 +- pubky-sdk/src/client/http_targets/wasm.rs | 6 +- pubky-sdk/src/lib.rs | 3 +- pubky-sdk/src/pubky.rs | 2 +- pubky-testnet/src/ephemeral_testnet.rs | 2 +- pubky-testnet/src/static_testnet.rs | 2 +- pubky-testnet/src/testnet.rs | 2 +- 74 files changed, 428 insertions(+), 199 deletions(-) create mode 100644 pubky-common/src/crypto/keys.rs diff --git a/examples/javascript/0-logging.mjs b/examples/javascript/0-logging.mjs index 419d5cf7..b760bb91 100644 --- a/examples/javascript/0-logging.mjs +++ b/examples/javascript/0-logging.mjs @@ -3,7 +3,7 @@ import { Pubky, Keypair, PublicKey, setLogLevel } from "@synonymdev/pubky"; import { args } from "./_cli.mjs"; -const TESTNET_HOMESERVER = "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; +const TESTNET_HOMESERVER = "pubky8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; const usage = ` Usage: @@ -43,7 +43,7 @@ const homeserver = PublicKey.from(homeserverArg); const keypair = Keypair.random(); const signer = pubky.signer(keypair); -console.log("Generated ephemeral signer:", keypair.publicKey.z32()); +console.log("Generated ephemeral signer:", keypair.publicKey.toString()); console.log("Signing up to homeserver... (watch the debug logs above)"); const session = await signer.signup(homeserver, null); diff --git a/examples/javascript/1-testnet.mjs b/examples/javascript/1-testnet.mjs index f7a0567d..d9965862 100644 --- a/examples/javascript/1-testnet.mjs +++ b/examples/javascript/1-testnet.mjs @@ -4,7 +4,7 @@ import { Pubky, Keypair, PublicKey, setLogLevel } from "@synonymdev/pubky"; // This is the default testnet homeserver. It comes from the secret `00000...` (bits). const TESTNET_HOMESERVER = - "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; + "pubky8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; // 1) Build Pubky SDK facade for local testnet host const pubky = Pubky.testnet(); @@ -14,7 +14,7 @@ const keypair = Keypair.random(); const signer = pubky.signer(keypair); const homeserver = PublicKey.from(TESTNET_HOMESERVER); const session = await signer.signup(homeserver, null); -console.log("Signed up succeeded for user:", session.info.publicKey.z32()); +console.log("Signed up succeeded for user:", session.info.publicKey.toString()); // 3) Write then read a file under /pub// const path = "/pub/my-cool-app/hello.txt"; diff --git a/examples/javascript/2-signup.mjs b/examples/javascript/2-signup.mjs index 670e557d..a278feab 100644 --- a/examples/javascript/2-signup.mjs +++ b/examples/javascript/2-signup.mjs @@ -8,7 +8,7 @@ Usage: npm run signup -- [signup_code] [--testnet] Example: - npm run signup -- 8pinxxg... ./alice.recovery INVITE-123 --testnet + npm run signup -- pubky8pinxxg... ./alice.recovery INVITE-123 --testnet `; const a = args(process.argv.slice(2), { usage }); @@ -32,5 +32,5 @@ const homeserver = PublicKey.from(homeserverArg); const session = await signer.signup(homeserver, signupCode ?? null); // 4) Show session owner + capabilities -console.log("\nSigned up as:", session.info.publicKey.z32()); +console.log("\nSigned up as:", session.info.publicKey.toString()); console.log("Capabilities:", session.info.capabilities); diff --git a/examples/javascript/3-authenticator.mjs b/examples/javascript/3-authenticator.mjs index 40e0e0a4..27d63f57 100644 --- a/examples/javascript/3-authenticator.mjs +++ b/examples/javascript/3-authenticator.mjs @@ -17,7 +17,7 @@ You can try this out with the example backend-less third party browser applicati const a = args(process.argv.slice(2), { usage, defaults: { - homeserver: "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo", + homeserver: "pubky8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo", }, }); const [recoveryPath, authUrl] = a._; diff --git a/examples/javascript/README.md b/examples/javascript/README.md index b5bdbfbb..7dc66a86 100644 --- a/examples/javascript/README.md +++ b/examples/javascript/README.md @@ -56,7 +56,7 @@ Decrypts a recovery file, creates a `Signer`, and signs up on a homeserver. npm run signup -- [invitation_code] [--testnet] # example (testnet homeserver) -npm run signup -- 8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo ./alice.recovery INVITE-123 --testnet +npm run signup -- pubky8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo ./alice.recovery INVITE-123 --testnet ``` You’ll be prompted for the recovery **passphrase**. @@ -80,14 +80,14 @@ You can run a Browser 3rd party app that requires authentication with [**3rd-par ### 4) Public storage read (no auth) -Reads a public resource via the **addressed** form: `/pub/my-cool-app/path/to/file.txt`. +Reads a public resource via the **addressed** form: `pubky/pub/my-cool-app/path/to/file.txt`. ```bash npm run storage -- / [--testnet] # examples -npm run storage -- q5oo7ma.../pub/my-cool-app/hello.txt --testnet -npm run storage -- operrr8w.../pub/pubky.app/posts/0033X02JAN0SG +npm run storage -- pubkyq5oo7ma.../pub/my-cool-app/hello.txt --testnet +npm run storage -- pubkyoperrr8w.../pub/pubky.app/posts/0033X02JAN0SG ``` Shows **exists**, **stats**, and downloads the content. @@ -96,8 +96,10 @@ Shows **exists**, **stats**, and downloads the content. Low-level fetch through the Pubky client. Handy for debugging. +> Use the **raw z-base32** key (no `pubky` prefix) in the `_pubky.` host portion. Call `publicKey.z32()` to get it. + ```bash -npm run request -- [--testnet] [-H "Name: value"]... [-d DATA] + npm run request -- [--testnet] [-H "Name: value"]... [-d DATA] # pubky:// read (testnet) npm run request -- GET https://_pubky.q5oo7ma.../pub/my-cool-app/info.json --testnet diff --git a/examples/rust/2-signup/README.md b/examples/rust/2-signup/README.md index 83e0aa7f..7fa215b5 100644 --- a/examples/rust/2-signup/README.md +++ b/examples/rust/2-signup/README.md @@ -11,5 +11,5 @@ as opposed to using a the 3rd party [authorization flow](../3-auth_flow). cargo run --bin signup # or use the local testnet defaults -cargo run --bin signup -- --testnet 8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo +cargo run --bin signup -- --testnet pubky8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo ``` diff --git a/examples/rust/2-signup/signup.rs b/examples/rust/2-signup/signup.rs index a42ee38a..5465835a 100644 --- a/examples/rust/2-signup/signup.rs +++ b/examples/rust/2-signup/signup.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Cli { - /// Homeserver Pkarr Domain (for example `5jsjx1o6fzu6aeeo697r3i5rx15zq41kikcye8wtwdqm4nb4tryo`) + /// Homeserver identifier (for example `pubky5jsjx1o6fzu6aeeo697r3i5rx15zq41kikcye8wtwdqm4nb4tryo`) homeserver: String, /// Path to a recovery_file of the Pubky you want to sign in with diff --git a/examples/rust/3-auth_flow/authenticator.rs b/examples/rust/3-auth_flow/authenticator.rs index 8495aff7..4c72d1da 100644 --- a/examples/rust/3-auth_flow/authenticator.rs +++ b/examples/rust/3-auth_flow/authenticator.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use url::Url; /// local testnet HOMESERVER -const HOMESERVER: &str = "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; +const HOMESERVER: &str = "pubky8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] diff --git a/examples/rust/5-request/README.md b/examples/rust/5-request/README.md index 74a6eee6..3948958c 100644 --- a/examples/rust/5-request/README.md +++ b/examples/rust/5-request/README.md @@ -17,8 +17,8 @@ cargo run --bin request -- [--testnet] [-H "Name: value"] [-d DAT ## Examples ```bash -# HTTPS to the _pubky (homeserver) subdomain form -cargo run --bin request -- GET https://_pubky./pub/my-cool-app/info.json +# HTTPS to the _pubky (homeserver) subdomain form (use the raw z-base32 key) +cargo run --bin request -- GET https://_pubky./pub/my-cool-app/info.json # JSON POST with headers cargo run --bin request -- \ @@ -28,7 +28,7 @@ cargo run --bin request -- \ POST https://example.com/data.json # Use local testnet endpoints -cargo run --bin request -- --testnet GET https://_pubky./pub/my-cool-app/hello.txt +cargo run --bin request -- --testnet GET https://_pubky./pub/my-cool-app/hello.txt ``` For example, at the time of writing, the following command returns the content of a user's social post from his pubky homeserver. diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index 465f281a..a6d0d4fd 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -6,7 +6,8 @@ use crypto_secretbox::{ }; use rand::random; -pub use pkarr::{Keypair, PublicKey}; +mod keys; +pub use keys::{Keypair, PublicKey}; pub use ed25519_dalek::Signature; diff --git a/pubky-common/src/crypto/keys.rs b/pubky-common/src/crypto/keys.rs new file mode 100644 index 00000000..97279ef0 --- /dev/null +++ b/pubky-common/src/crypto/keys.rs @@ -0,0 +1,198 @@ +use core::fmt; +use core::ops::{Deref, DerefMut}; +use core::str::FromStr; +#[cfg(not(target_arch = "wasm32"))] +use std::{io, path::Path}; + +use serde::{Deserialize, Serialize}; + +type ParseError = >::Error; + +fn parse_public_key(value: &str) -> Result { + let raw = value.strip_prefix("pubky").unwrap_or(value); + pkarr::PublicKey::try_from(raw.to_string()) +} + +/// Wrapper around [`pkarr::Keypair`] that customizes [`PublicKey`] rendering. +#[derive(Clone)] +pub struct Keypair(pkarr::Keypair); + +impl Keypair { + /// Generate a random keypair. + #[must_use] + pub fn random() -> Self { + Self(pkarr::Keypair::random()) + } + + /// Construct a [`Keypair`] from a 32-byte secret key. + #[must_use] + pub fn from_secret_key(secret: &[u8; 32]) -> Self { + Self(pkarr::Keypair::from_secret_key(secret)) + } + + /// Read a keypair from a pkarr secret key file. + #[cfg(not(target_arch = "wasm32"))] + pub fn from_secret_key_file(path: &Path) -> Result { + pkarr::Keypair::from_secret_key_file(path).map(Self) + } + + /// Return the [`PublicKey`] associated with this [`Keypair`]. + /// + /// Display the returned key with `.to_string()` to get the `pubky` identifier or + /// [`PublicKey::z32()`] when you specifically need the bare z-base32 text (e.g. hostnames). + #[must_use] + pub fn public_key(&self) -> PublicKey { + PublicKey(self.0.public_key()) + } + + /// Borrow the inner [`pkarr::Keypair`]. + #[must_use] + pub const fn as_inner(&self) -> &pkarr::Keypair { + &self.0 + } + + /// Persist the secret key to disk using the pkarr format. + #[cfg(not(target_arch = "wasm32"))] + pub fn write_secret_key_file(&self, path: &Path) -> Result<(), io::Error> { + self.0.write_secret_key_file(path) + } + + /// Extract the inner [`pkarr::Keypair`]. + #[must_use] + pub fn into_inner(self) -> pkarr::Keypair { + self.0 + } +} + +impl fmt::Debug for Keypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Deref for Keypair { + type Target = pkarr::Keypair; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Keypair { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Keypair { + fn from(keypair: pkarr::Keypair) -> Self { + Self(keypair) + } +} + +impl From for pkarr::Keypair { + fn from(value: Keypair) -> Self { + value.0 + } +} + +/// Wrapper around [`pkarr::PublicKey`] that renders with the `pubky` prefix. +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PublicKey(pkarr::PublicKey); + +impl PublicKey { + /// Borrow the inner [`pkarr::PublicKey`]. + #[must_use] + pub const fn as_inner(&self) -> &pkarr::PublicKey { + &self.0 + } + + /// Extract the inner [`pkarr::PublicKey`]. + #[must_use] + pub fn into_inner(self) -> pkarr::PublicKey { + self.0 + } + + /// Return the raw z-base32 representation without the `pubky` prefix. + #[must_use] + pub fn z32(&self) -> String { + self.0.to_string() + } +} + +impl fmt::Display for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "pubky{}", self.z32()) + } +} + +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PublicKey").field(&self.to_string()).finish() + } +} + +impl Deref for PublicKey { + type Target = pkarr::PublicKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for PublicKey { + fn from(value: pkarr::PublicKey) -> Self { + Self(value) + } +} + +impl From<&pkarr::PublicKey> for PublicKey { + fn from(value: &pkarr::PublicKey) -> Self { + Self(value.clone()) + } +} + +impl From for pkarr::PublicKey { + fn from(value: PublicKey) -> Self { + value.0 + } +} + +impl From<&PublicKey> for pkarr::PublicKey { + fn from(value: &PublicKey) -> Self { + value.0.clone() + } +} + +impl TryFrom<&str> for PublicKey { + type Error = ParseError; + + fn try_from(value: &str) -> Result { + parse_public_key(value).map(Self) + } +} + +impl TryFrom<&String> for PublicKey { + type Error = ParseError; + + fn try_from(value: &String) -> Result { + parse_public_key(value).map(Self) + } +} + +impl TryFrom for PublicKey { + type Error = ParseError; + + fn try_from(value: String) -> Result { + parse_public_key(&value).map(Self) + } +} + +impl FromStr for PublicKey { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + parse_public_key(s).map(Self) + } +} diff --git a/pubky-common/src/recovery_file.rs b/pubky-common/src/recovery_file.rs index 3e889e8c..848b9c8b 100644 --- a/pubky-common/src/recovery_file.rs +++ b/pubky-common/src/recovery_file.rs @@ -1,9 +1,8 @@ //! Tools for encrypting and decrypting a recovery file storing user's root key's secret. use argon2::Argon2; -use pkarr::Keypair; -use crate::crypto::{decrypt, encrypt}; +use crate::crypto::{decrypt, encrypt, Keypair}; static SPEC_NAME: &str = "recovery"; static SPEC_LINE: &str = "pubky.org/recovery"; diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 0024981b..1c202d56 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -1,6 +1,5 @@ //! Pubky homeserver session struct. -use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; @@ -9,6 +8,7 @@ use alloc::vec::Vec; use crate::{ capabilities::{Capabilities, Capability}, + crypto::PublicKey, timestamp::Timestamp, }; diff --git a/pubky-homeserver/src/admin_server/routes/delete_entry.rs b/pubky-homeserver/src/admin_server/routes/delete_entry.rs index c73963cb..030408b9 100644 --- a/pubky-homeserver/src/admin_server/routes/delete_entry.rs +++ b/pubky-homeserver/src/admin_server/routes/delete_entry.rs @@ -29,7 +29,7 @@ mod tests { use crate::AppContext; use axum::{routing::delete, Router}; use opendal::Buffer; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; async fn write_test_file(file_service: &FileService, entry_path: &EntryPath) { let buffer = Buffer::from(vec![0; 10]); diff --git a/pubky-homeserver/src/admin_server/routes/disable_users.rs b/pubky-homeserver/src/admin_server/routes/disable_users.rs index d74ee2c4..bae8e96e 100644 --- a/pubky-homeserver/src/admin_server/routes/disable_users.rs +++ b/pubky-homeserver/src/admin_server/routes/disable_users.rs @@ -74,7 +74,7 @@ mod tests { use crate::{persistence::files::FileService, AppContext}; use axum::routing::post; use axum::Router; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; #[tokio::test] #[pubky_test_utils::test] diff --git a/pubky-homeserver/src/app_context.rs b/pubky-homeserver/src/app_context.rs index f9931893..c8d93e47 100644 --- a/pubky-homeserver/src/app_context.rs +++ b/pubky-homeserver/src/app_context.rs @@ -15,7 +15,7 @@ use crate::{ }, ConfigToml, DataDir, }; -use pkarr::Keypair; +use pubky_common::crypto::Keypair; use std::{sync::Arc, time::Duration}; /// Errors that can occur when converting a `DataDir` to an `AppContext`. diff --git a/pubky-homeserver/src/client_server/err_if_user_is_invalid.rs b/pubky-homeserver/src/client_server/err_if_user_is_invalid.rs index 23feb61e..09c5a44c 100644 --- a/pubky-homeserver/src/client_server/err_if_user_is_invalid.rs +++ b/pubky-homeserver/src/client_server/err_if_user_is_invalid.rs @@ -5,7 +5,7 @@ use crate::{ }, shared::{HttpError, HttpResult}, }; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; /// Returns the user if it exists and is not disabled, otherwise returns an error. /// - User doesn't exist: returns 404 diff --git a/pubky-homeserver/src/client_server/extractors.rs b/pubky-homeserver/src/client_server/extractors.rs index 0f328d5b..6aae3f22 100644 --- a/pubky-homeserver/src/client_server/extractors.rs +++ b/pubky-homeserver/src/client_server/extractors.rs @@ -7,7 +7,7 @@ use axum::{ RequestPartsExt, }; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use crate::shared::parse_bool; diff --git a/pubky-homeserver/src/client_server/layers/authz.rs b/pubky-homeserver/src/client_server/layers/authz.rs index 2b10a1a9..23013f72 100644 --- a/pubky-homeserver/src/client_server/layers/authz.rs +++ b/pubky-homeserver/src/client_server/layers/authz.rs @@ -8,7 +8,7 @@ use axum::{ http::{Request, StatusCode}, }; use futures_util::future::BoxFuture; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use std::{convert::Infallible, task::Poll}; use tower::{Layer, Service}; use tower_cookies::Cookies; diff --git a/pubky-homeserver/src/client_server/layers/pubky_host.rs b/pubky-homeserver/src/client_server/layers/pubky_host.rs index 781ca957..d6d1a237 100644 --- a/pubky-homeserver/src/client_server/layers/pubky_host.rs +++ b/pubky-homeserver/src/client_server/layers/pubky_host.rs @@ -1,7 +1,7 @@ use crate::client_server::extractors::PubkyHost; use axum::{body::Body, http::Request}; use futures_util::future::BoxFuture; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use std::{convert::Infallible, task::Poll}; use tower::{Layer, Service}; diff --git a/pubky-homeserver/src/client_server/routes/auth.rs b/pubky-homeserver/src/client_server/routes/auth.rs index 06bd566a..43f808d0 100644 --- a/pubky-homeserver/src/client_server/routes/auth.rs +++ b/pubky-homeserver/src/client_server/routes/auth.rs @@ -17,8 +17,8 @@ use axum::{ }; use axum_extra::extract::Host; use bytes::Bytes; -use pkarr::PublicKey; use pubky_common::capabilities::Capabilities; +use pubky_common::crypto::PublicKey; use pubky_common::session::SessionInfo; use std::collections::HashMap; use tower_cookies::{ @@ -188,7 +188,7 @@ fn is_secure(host: &str) -> bool { #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use super::*; diff --git a/pubky-homeserver/src/client_server/routes/events.rs b/pubky-homeserver/src/client_server/routes/events.rs index 16d82968..1292f6ec 100644 --- a/pubky-homeserver/src/client_server/routes/events.rs +++ b/pubky-homeserver/src/client_server/routes/events.rs @@ -8,7 +8,7 @@ use axum::{ }, }; use futures_util::stream::Stream; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use serde::Deserialize; use std::{collections::HashMap, convert::Infallible}; use url::form_urlencoded; diff --git a/pubky-homeserver/src/client_server/routes/tenants/read.rs b/pubky-homeserver/src/client_server/routes/tenants/read.rs index 8d370037..d6aa975f 100644 --- a/pubky-homeserver/src/client_server/routes/tenants/read.rs +++ b/pubky-homeserver/src/client_server/routes/tenants/read.rs @@ -236,7 +236,9 @@ mod tests { use axum::Router; use axum_test::TestServer; use pkarr::{Keypair, PublicKey}; - use pubky_common::{auth::AuthToken, capabilities::Capability}; + use pubky_common::{ + auth::AuthToken, capabilities::Capability, crypto::Keypair as PubkyKeypair, + }; use crate::app_context::AppContext; use crate::client_server::ClientServer; @@ -245,7 +247,10 @@ mod tests { server: &axum_test::TestServer, keypair: &Keypair, ) -> anyhow::Result { - let auth_token = AuthToken::sign(keypair, vec![Capability::root()]); + let auth_token = AuthToken::sign( + &PubkyKeypair::from(keypair.clone()), + vec![Capability::root()], + ); let body_bytes: axum::body::Bytes = auth_token.serialize().into(); let response = server .post("/signup") diff --git a/pubky-homeserver/src/client_server/routes/tenants/write.rs b/pubky-homeserver/src/client_server/routes/tenants/write.rs index 389094cf..0e5d998b 100644 --- a/pubky-homeserver/src/client_server/routes/tenants/write.rs +++ b/pubky-homeserver/src/client_server/routes/tenants/write.rs @@ -110,7 +110,7 @@ pub async fn fail_if_size_hint_bigger_than_user_quota<'a>( #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use crate::{persistence::sql::SqlDb, shared::webdav::WebDavPath}; diff --git a/pubky-homeserver/src/data_directory/data_dir.rs b/pubky-homeserver/src/data_directory/data_dir.rs index 12a785b8..75864ece 100644 --- a/pubky-homeserver/src/data_directory/data_dir.rs +++ b/pubky-homeserver/src/data_directory/data_dir.rs @@ -19,7 +19,7 @@ pub trait DataDir: std::fmt::Debug + DynClone + Send + Sync { /// Reads the secret file from the data directory. /// Creates a new secret file if it doesn't exist. - fn read_or_create_keypair(&self) -> anyhow::Result; + fn read_or_create_keypair(&self) -> anyhow::Result; } dyn_clone::clone_trait_object!(DataDir); diff --git a/pubky-homeserver/src/data_directory/mock_data_dir.rs b/pubky-homeserver/src/data_directory/mock_data_dir.rs index 7112f401..1603f2a1 100644 --- a/pubky-homeserver/src/data_directory/mock_data_dir.rs +++ b/pubky-homeserver/src/data_directory/mock_data_dir.rs @@ -13,7 +13,7 @@ pub struct MockDataDir { /// The configuration for the homeserver. pub config_toml: super::ConfigToml, /// The keypair for the homeserver. - pub keypair: pkarr::Keypair, + pub keypair: pubky_common::crypto::Keypair, } impl MockDataDir { @@ -22,9 +22,9 @@ impl MockDataDir { /// If keypair is not provided, a new one will be generated. pub fn new( config_toml: super::ConfigToml, - keypair: Option, + keypair: Option, ) -> anyhow::Result { - let keypair = keypair.unwrap_or_else(pkarr::Keypair::random); + let keypair = keypair.unwrap_or_else(pubky_common::crypto::Keypair::random); Ok(Self { temp_dir: std::sync::Arc::new(tempfile::TempDir::new()?), config_toml, @@ -36,7 +36,7 @@ impl MockDataDir { #[cfg(any(test, feature = "testing"))] pub fn test() -> Self { let config = super::ConfigToml::test(); - let keypair = pkarr::Keypair::from_secret_key(&[0; 32]); + let keypair = pubky_common::crypto::Keypair::from_secret_key(&[0; 32]); Self::new(config, Some(keypair)).expect("failed to create MockDataDir") } } @@ -60,7 +60,7 @@ impl DataDir for MockDataDir { Ok(self.config_toml.clone()) } - fn read_or_create_keypair(&self) -> anyhow::Result { + fn read_or_create_keypair(&self) -> anyhow::Result { Ok(self.keypair.clone()) } } diff --git a/pubky-homeserver/src/data_directory/persistent_data_dir.rs b/pubky-homeserver/src/data_directory/persistent_data_dir.rs index 3acbe62c..5b50eace 100644 --- a/pubky-homeserver/src/data_directory/persistent_data_dir.rs +++ b/pubky-homeserver/src/data_directory/persistent_data_dir.rs @@ -103,15 +103,15 @@ impl DataDir for PersistentDataDir { } /// Reads the secret file. Creates a new secret file if it doesn't exist. - fn read_or_create_keypair(&self) -> anyhow::Result { + fn read_or_create_keypair(&self) -> anyhow::Result { let secret_file_path = self.get_secret_file_path(); if !secret_file_path.exists() { // Create a new secret file - pkarr::Keypair::random().write_secret_key_file(&secret_file_path)?; + pubky_common::crypto::Keypair::random().write_secret_key_file(&secret_file_path)?; tracing::info!("Secret file created at {}", secret_file_path.display()); } // Read the secret file - let keypair = pkarr::Keypair::from_secret_key_file(&secret_file_path)?; + let keypair = pubky_common::crypto::Keypair::from_secret_key_file(&secret_file_path)?; Ok(keypair) } } @@ -230,7 +230,7 @@ mod tests { data_dir.ensure_data_dir_exists_and_is_writable().unwrap(); // Create a secret file - let keypair = pkarr::Keypair::random(); + let keypair = pubky_common::crypto::Keypair::random(); let secret_file_path = data_dir.get_secret_file_path(); let file_content = format!("\n {}\n \n", hex::encode(keypair.secret_key())); std::fs::write(secret_file_path.clone(), file_content).unwrap(); diff --git a/pubky-homeserver/src/data_directory/quota_config/limit_key.rs b/pubky-homeserver/src/data_directory/quota_config/limit_key.rs index 3e225039..b6a45291 100644 --- a/pubky-homeserver/src/data_directory/quota_config/limit_key.rs +++ b/pubky-homeserver/src/data_directory/quota_config/limit_key.rs @@ -2,7 +2,7 @@ use std::fmt; use std::net::IpAddr; use std::str::FromStr; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; /// The key to limit the quota on. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -47,7 +47,7 @@ impl fmt::Display for LimitKey { f, "{}", match self { - LimitKey::User(user_pubkey) => user_pubkey.to_string(), + LimitKey::User(user_pubkey) => user_pubkey.z32(), LimitKey::Ip(ip_addr) => ip_addr.to_string(), } ) @@ -130,7 +130,7 @@ impl<'de> serde::Deserialize<'de> for LimitKeyType { mod tests { use std::net::Ipv4Addr; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use super::*; diff --git a/pubky-homeserver/src/data_directory/quota_config/path_limit.rs b/pubky-homeserver/src/data_directory/quota_config/path_limit.rs index b5103141..7b112560 100644 --- a/pubky-homeserver/src/data_directory/quota_config/path_limit.rs +++ b/pubky-homeserver/src/data_directory/quota_config/path_limit.rs @@ -98,7 +98,7 @@ mod tests { str::FromStr, }; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use super::*; diff --git a/pubky-homeserver/src/homeserver_app.rs b/pubky-homeserver/src/homeserver_app.rs index 43a864f8..bd85a51a 100644 --- a/pubky-homeserver/src/homeserver_app.rs +++ b/pubky-homeserver/src/homeserver_app.rs @@ -8,7 +8,7 @@ use crate::tracing::init_tracing_logs_with_config_if_set; use crate::MockDataDir; use crate::{app_context::AppContext, data_directory::PersistentDataDir}; use anyhow::Result; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use std::path::PathBuf; use std::time::Duration; diff --git a/pubky-homeserver/src/persistence/files/entry/entry_layer.rs b/pubky-homeserver/src/persistence/files/entry/entry_layer.rs index a8ad3cf3..ea1dce6c 100644 --- a/pubky-homeserver/src/persistence/files/entry/entry_layer.rs +++ b/pubky-homeserver/src/persistence/files/entry/entry_layer.rs @@ -243,7 +243,7 @@ mod tests { let layer = EntryLayer::new(db.clone()); let operator = operator.layer(layer); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); diff --git a/pubky-homeserver/src/persistence/files/events/events_entity.rs b/pubky-homeserver/src/persistence/files/events/events_entity.rs index 01575e98..758222e8 100644 --- a/pubky-homeserver/src/persistence/files/events/events_entity.rs +++ b/pubky-homeserver/src/persistence/files/events/events_entity.rs @@ -1,7 +1,7 @@ use std::str::FromStr; -use pkarr::PublicKey; use pubky_common::crypto::Hash; +use pubky_common::crypto::PublicKey; use sea_query::Iden; use sqlx::{postgres::PgRow, FromRow, Row}; diff --git a/pubky-homeserver/src/persistence/files/events/events_layer.rs b/pubky-homeserver/src/persistence/files/events/events_layer.rs index 67533854..1bb230fd 100644 --- a/pubky-homeserver/src/persistence/files/events/events_layer.rs +++ b/pubky-homeserver/src/persistence/files/events/events_layer.rs @@ -280,7 +280,7 @@ mod tests { let layer = EventsLayer::new(db.clone(), events_service); let operator = operator.layer(layer); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); diff --git a/pubky-homeserver/src/persistence/files/events/events_repository.rs b/pubky-homeserver/src/persistence/files/events/events_repository.rs index 6846f82d..246d55ec 100644 --- a/pubky-homeserver/src/persistence/files/events/events_repository.rs +++ b/pubky-homeserver/src/persistence/files/events/events_repository.rs @@ -333,7 +333,7 @@ impl Display for EventType { #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use crate::{ persistence::sql::{user::UserRepository, SqlDb}, diff --git a/pubky-homeserver/src/persistence/files/events/events_service.rs b/pubky-homeserver/src/persistence/files/events/events_service.rs index 0ad89279..72486371 100644 --- a/pubky-homeserver/src/persistence/files/events/events_service.rs +++ b/pubky-homeserver/src/persistence/files/events/events_service.rs @@ -113,7 +113,7 @@ mod tests { use super::*; use crate::persistence::sql::{user::UserRepository, SqlDb}; use crate::shared::webdav::WebDavPath; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; #[tokio::test] #[pubky_test_utils::test] diff --git a/pubky-homeserver/src/persistence/files/file/file_service.rs b/pubky-homeserver/src/persistence/files/file/file_service.rs index 2be2dedd..865eabc3 100644 --- a/pubky-homeserver/src/persistence/files/file/file_service.rs +++ b/pubky-homeserver/src/persistence/files/file/file_service.rs @@ -147,7 +147,7 @@ mod tests { let context = AppContext::test().await; let file_service = FileService::new_from_context(&context).unwrap(); let db = context.sql_db.clone(); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); let user = UserRepository::create(&pubkey, &mut db.pool().into()) .await @@ -263,7 +263,7 @@ mod tests { let file_service = FileService::new_from_context(&context).unwrap(); let db = context.sql_db.clone(); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); @@ -295,7 +295,7 @@ mod tests { let file_service = FileService::new_from_context(&context).unwrap(); let db = context.sql_db.clone(); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); @@ -327,7 +327,7 @@ mod tests { let file_service = FileService::new_from_context(&context).unwrap(); let db = context.sql_db.clone(); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); @@ -362,7 +362,7 @@ mod tests { let file_service = FileService::new_from_context(&context).unwrap(); let db = context.sql_db.clone(); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); @@ -390,7 +390,7 @@ mod tests { let file_service = FileService::new_from_context(&context).unwrap(); let db = context.sql_db.clone(); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); @@ -425,7 +425,7 @@ mod tests { let file_service = FileService::new_from_context(&context).unwrap(); let db = context.sql_db.clone(); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); diff --git a/pubky-homeserver/src/persistence/files/opendal/opendal_service.rs b/pubky-homeserver/src/persistence/files/opendal/opendal_service.rs index 053bf265..f759e090 100644 --- a/pubky-homeserver/src/persistence/files/opendal/opendal_service.rs +++ b/pubky-homeserver/src/persistence/files/opendal/opendal_service.rs @@ -283,7 +283,7 @@ mod tests { let service = OpendalService::new(&context).expect("Failed to create OpenDAL service for testing"); - let pubky = pkarr::Keypair::random().public_key(); + let pubky = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubky, &mut context.sql_db.pool().into()) .await .unwrap(); @@ -300,7 +300,7 @@ mod tests { context.config_toml.general.user_storage_quota_mb = 1; let service = OpendalService::new(&context).expect("Failed to create OpenDAL service for testing"); - let pubky = pkarr::Keypair::random().public_key(); + let pubky = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubky, &mut context.sql_db.pool().into()) .await .unwrap(); @@ -321,7 +321,7 @@ mod tests { for (_scheme, operator) in operators.operators() { let file_service = OpendalService::new_from_operator(operator); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); let path = EntryPath::new(pubkey, WebDavPath::new("/test.txt").unwrap()); // Write a 10KB file filled with test data @@ -383,7 +383,7 @@ mod tests { for (_scheme, operator) in operators.operators() { let file_service = OpendalService::new_from_operator(operator); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); let path = EntryPath::new(pubkey, WebDavPath::new("/test_stream.txt").unwrap()); // Create test data - multiple chunks to test streaming diff --git a/pubky-homeserver/src/persistence/files/user_quota_layer.rs b/pubky-homeserver/src/persistence/files/user_quota_layer.rs index 56715ca2..f63f259f 100644 --- a/pubky-homeserver/src/persistence/files/user_quota_layer.rs +++ b/pubky-homeserver/src/persistence/files/user_quota_layer.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; use std::sync::Arc; +use pubky_common::crypto::PublicKey; + use crate::persistence::files::utils::ensure_valid_path; use crate::persistence::sql::SqlDb; use crate::persistence::sql::{uexecutor, user::UserRepository}; @@ -63,17 +65,19 @@ impl LayeredAccess for UserQuotaAccessor { } async fn create_dir(&self, path: &str, args: OpCreateDir) -> Result { - ensure_valid_path(path)?; - self.inner.create_dir(path, args).await + let entry_path = ensure_valid_path(path)?; + self.inner.create_dir(entry_path.as_str(), args).await } async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> { - self.inner.read(path, args).await + let entry_path = ensure_valid_path(path)?; + self.inner.read(entry_path.as_str(), args).await } async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> { let entry_path = ensure_valid_path(path)?; - let (rp, writer) = self.inner.write(path, args).await?; + let canonical_path = entry_path.to_string(); + let (rp, writer) = self.inner.write(&canonical_path, args).await?; Ok(( rp, WriterWrapper { @@ -88,17 +92,20 @@ impl LayeredAccess for UserQuotaAccessor { } async fn copy(&self, from: &str, to: &str, args: OpCopy) -> Result { - let _ = ensure_valid_path(to)?; - self.inner.copy(from, to, args).await + let from = ensure_valid_path(from)?; + let to = ensure_valid_path(to)?; + self.inner.copy(from.as_str(), to.as_str(), args).await } async fn rename(&self, from: &str, to: &str, args: OpRename) -> Result { - let _ = ensure_valid_path(to)?; - self.inner.rename(from, to, args).await + let from = ensure_valid_path(from)?; + let to = ensure_valid_path(to)?; + self.inner.rename(from.as_str(), to.as_str(), args).await } async fn stat(&self, path: &str, args: OpStat) -> Result { - self.inner.stat(path, args).await + let entry_path = ensure_valid_path(path)?; + self.inner.stat(entry_path.as_str(), args).await } async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> { @@ -119,7 +126,8 @@ impl LayeredAccess for UserQuotaAccessor { } async fn presign(&self, path: &str, args: OpPresign) -> Result { - self.inner.presign(path, args).await + let entry_path = ensure_valid_path(path)?; + self.inner.presign(entry_path.as_str(), args).await } } @@ -272,7 +280,7 @@ pub struct DeleterWrapper { impl DeleterWrapper { async fn update_user_quota(&self, deleted_paths: Vec) -> Result<()> { // Group deleted paths by user pubkey - let mut user_paths: HashMap> = HashMap::new(); + let mut user_paths: HashMap> = HashMap::new(); for path in deleted_paths { user_paths .entry(path.entry_path.pubkey().clone()) @@ -351,7 +359,7 @@ impl oio::Delete for DeleterWrapper { )); } }; - self.inner.delete(path, args)?; + self.inner.delete(helper.entry_path.as_str(), args)?; self.path_queue.push(helper); Ok(()) } @@ -365,10 +373,7 @@ mod tests { use super::*; - async fn get_user_data_usage( - db: &SqlDb, - user_pubkey: &pkarr::PublicKey, - ) -> anyhow::Result { + async fn get_user_data_usage(db: &SqlDb, user_pubkey: &PublicKey) -> anyhow::Result { let user = UserRepository::get(user_pubkey, &mut db.pool().into()) .await .map_err(|e| opendal::Error::new(opendal::ErrorKind::Unexpected, e.to_string()))?; @@ -387,7 +392,7 @@ mod tests { .write("1234567890/test.txt", vec![0; 10]) .await .expect_err("Should fail because the path doesn't start with a pubkey"); - let pubkey = pkarr::Keypair::random().public_key(); + let pubkey = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); @@ -409,7 +414,7 @@ mod tests { let layer = UserQuotaLayer::new(db.clone(), 1024 * 1024); let operator = get_memory_operator().layer(layer); - let user_pubkey1 = pkarr::Keypair::random().public_key(); + let user_pubkey1 = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&user_pubkey1, &mut db.pool().into()) .await .unwrap(); @@ -462,7 +467,7 @@ mod tests { let layer = UserQuotaLayer::new(db.clone(), 20 + FILE_METADATA_SIZE); let operator = get_memory_operator().layer(layer); - let user_pubkey1 = pkarr::Keypair::random().public_key(); + let user_pubkey1 = pubky_common::crypto::Keypair::random().public_key(); UserRepository::create(&user_pubkey1, &mut db.pool().into()) .await .unwrap(); diff --git a/pubky-homeserver/src/persistence/lmdb/migrations/m220420251247_add_user_disabled_used_bytes.rs b/pubky-homeserver/src/persistence/lmdb/migrations/m220420251247_add_user_disabled_used_bytes.rs index 2f2cd04c..1915577b 100644 --- a/pubky-homeserver/src/persistence/lmdb/migrations/m220420251247_add_user_disabled_used_bytes.rs +++ b/pubky-homeserver/src/persistence/lmdb/migrations/m220420251247_add_user_disabled_used_bytes.rs @@ -1,8 +1,8 @@ use super::super::tables::users; use crate::persistence::lmdb::tables::users::PublicKeyCodec; use heed::{BoxedError, BytesDecode, BytesEncode, Database, Env, RwTxn}; -use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; +use pubky_common::crypto::PublicKey; use serde::{Deserialize, Serialize}; use std::borrow::Cow; @@ -103,7 +103,7 @@ fn read_old_users_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result = vec![]; for entry in table.iter(wtxn)? { let (key, old_user) = entry?; - new_users.push((key, old_user)); + new_users.push((key.into(), old_user)); } Ok(new_users) @@ -152,7 +152,7 @@ pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { #[cfg(test)] mod tests { use heed::EnvOpenOptions; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use crate::persistence::lmdb::{db::DEFAULT_MAP_SIZE, migrations::m0}; diff --git a/pubky-homeserver/src/persistence/lmdb/sql_migrator/entries.rs b/pubky-homeserver/src/persistence/lmdb/sql_migrator/entries.rs index bd711d6f..481ee09b 100644 --- a/pubky-homeserver/src/persistence/lmdb/sql_migrator/entries.rs +++ b/pubky-homeserver/src/persistence/lmdb/sql_migrator/entries.rs @@ -74,7 +74,7 @@ pub async fn migrate_entries<'a>( #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use pubky_common::{crypto::Hash, timestamp::Timestamp}; use crate::{ diff --git a/pubky-homeserver/src/persistence/lmdb/sql_migrator/events.rs b/pubky-homeserver/src/persistence/lmdb/sql_migrator/events.rs index 01f36cd5..7de0edee 100644 --- a/pubky-homeserver/src/persistence/lmdb/sql_migrator/events.rs +++ b/pubky-homeserver/src/persistence/lmdb/sql_migrator/events.rs @@ -92,7 +92,7 @@ pub async fn migrate_events<'a>( #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use pubky_common::timestamp::Timestamp; use sqlx::types::chrono::DateTime; diff --git a/pubky-homeserver/src/persistence/lmdb/sql_migrator/sessions.rs b/pubky-homeserver/src/persistence/lmdb/sql_migrator/sessions.rs index 76c544f0..e413d748 100644 --- a/pubky-homeserver/src/persistence/lmdb/sql_migrator/sessions.rs +++ b/pubky-homeserver/src/persistence/lmdb/sql_migrator/sessions.rs @@ -71,8 +71,8 @@ pub async fn migrate_sessions<'a>( mod tests { use std::time::{SystemTime, UNIX_EPOCH}; - use pkarr::Keypair; use pubky_common::capabilities::{Capabilities, Capability}; + use pubky_common::crypto::Keypair; use crate::persistence::sql::{ session::{SessionRepository, SessionSecret}, diff --git a/pubky-homeserver/src/persistence/lmdb/sql_migrator/signup_codes.rs b/pubky-homeserver/src/persistence/lmdb/sql_migrator/signup_codes.rs index 1314d408..4076d7b8 100644 --- a/pubky-homeserver/src/persistence/lmdb/sql_migrator/signup_codes.rs +++ b/pubky-homeserver/src/persistence/lmdb/sql_migrator/signup_codes.rs @@ -18,7 +18,7 @@ pub async fn create<'a>( executor: &mut UnifiedExecutor<'a>, ) -> Result<(), sqlx::Error> { let used_by = match lmdb_token.used.as_ref() { - Some(p) => SimpleExpr::Value(p.to_string().into()), + Some(p) => SimpleExpr::Value(p.z32().into()), None => SimpleExpr::Value(Value::String(None)), }; let created_at = @@ -66,7 +66,7 @@ pub async fn migrate_signup_codes<'a>( mod tests { use std::time::{SystemTime, UNIX_EPOCH}; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use crate::persistence::sql::{ signup_code::{SignupCodeId, SignupCodeRepository}, diff --git a/pubky-homeserver/src/persistence/lmdb/sql_migrator/users.rs b/pubky-homeserver/src/persistence/lmdb/sql_migrator/users.rs index b0e36f55..90cba211 100644 --- a/pubky-homeserver/src/persistence/lmdb/sql_migrator/users.rs +++ b/pubky-homeserver/src/persistence/lmdb/sql_migrator/users.rs @@ -9,6 +9,7 @@ use crate::persistence::{ UnifiedExecutor, }, }; +use pubky_common::crypto::PublicKey; /// Convert nano seconds to a timestamp. pub fn nano_seconds_to_timestamp(nano_seconds: u64) -> Option> { @@ -56,6 +57,7 @@ pub async fn migrate_users<'a>( let mut count = 0; for record in lmdb.tables.users.iter(&lmdb_txn)? { let (public_key, lmdb_user) = record?; + let public_key: PublicKey = public_key.into(); let mut sql_user = UserRepository::create(&public_key, executor).await?; sql_user.created_at = nano_seconds_to_timestamp(lmdb_user.created_at) .expect("Failed to convert nano seconds to timestamp") @@ -73,7 +75,7 @@ pub async fn migrate_users<'a>( mod tests { use std::time::{SystemTime, UNIX_EPOCH}; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use crate::persistence::{lmdb::tables::users::User, sql::SqlDb}; @@ -88,14 +90,16 @@ mod tests { let mut wtxn = lmdb.env.write_txn().unwrap(); // User1 let user1_pubkey = Keypair::random().public_key(); - let mut lmdb_user1 = User::default(); - lmdb_user1.created_at = SystemTime::now() + let user1_created_at = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() * 1_000_000; - lmdb_user1.used_bytes = 100; - lmdb_user1.disabled = true; + let lmdb_user1 = User { + created_at: user1_created_at, + used_bytes: 100, + disabled: true, + }; lmdb.tables .users .put(&mut wtxn, &user1_pubkey, &lmdb_user1) @@ -103,14 +107,16 @@ mod tests { // User2 let user2_pubkey = Keypair::random().public_key(); - let mut lmdb_user2 = User::default(); - lmdb_user2.created_at = SystemTime::now() + let user2_created_at = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() * 1_000_000; - lmdb_user2.used_bytes = 200; - lmdb_user2.disabled = false; + let lmdb_user2 = User { + created_at: user2_created_at, + used_bytes: 200, + disabled: false, + }; lmdb.tables .users .put(&mut wtxn, &user2_pubkey, &lmdb_user2) diff --git a/pubky-homeserver/src/persistence/lmdb/tables/signup_tokens.rs b/pubky-homeserver/src/persistence/lmdb/tables/signup_tokens.rs index 4f883a7c..625640d3 100644 --- a/pubky-homeserver/src/persistence/lmdb/tables/signup_tokens.rs +++ b/pubky-homeserver/src/persistence/lmdb/tables/signup_tokens.rs @@ -2,8 +2,8 @@ use heed::{ types::{Bytes, Str}, Database, }; -use pkarr::PublicKey; use postcard::from_bytes; +use pubky_common::crypto::PublicKey; use serde::{Deserialize, Serialize}; diff --git a/pubky-homeserver/src/persistence/sql/entities/entry/entity.rs b/pubky-homeserver/src/persistence/sql/entities/entry/entity.rs index 8367dcf1..44915df0 100644 --- a/pubky-homeserver/src/persistence/sql/entities/entry/entity.rs +++ b/pubky-homeserver/src/persistence/sql/entities/entry/entity.rs @@ -1,4 +1,4 @@ -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use sea_query::Iden; use sqlx::{postgres::PgRow, FromRow, Row}; diff --git a/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs b/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs index 651a2cd6..f18dba3c 100644 --- a/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs +++ b/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs @@ -110,7 +110,7 @@ impl EntryRepository { Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))), ) .and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).eq(path.path().as_str())) - .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().to_string())) + .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32())) .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); let con = executor.get_con().await?; @@ -179,7 +179,7 @@ impl EntryRepository { Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))), ) .and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).eq(path.path().as_str())) - .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().to_string())) + .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32())) .to_owned(); // Then delete the entry by the id @@ -213,7 +213,7 @@ impl EntryRepository { Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))), ) .and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).like(format!("{}%", full_path))) // Everything that starts with the path - .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().to_string())) + .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32())) .limit(1) .to_owned(); @@ -256,7 +256,7 @@ impl EntryRepository { Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))), ) .and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).like(format!("{}%", dir_path))) // Everything that starts with the path - .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().to_string())) + .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32())) .to_owned(); // Use a select in select to filter the previous regex regpath @@ -333,7 +333,7 @@ impl EntryRepository { Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))), ) .and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).like(format!("{}%", full_path))) // Everything that starts with the path - .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().to_string())) + .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32())) .to_owned(); if reverse { @@ -398,7 +398,7 @@ pub enum EntryIden { mod tests { use super::*; use crate::persistence::sql::{entities::user::UserRepository, SqlDb}; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use std::collections::HashSet; #[tokio::test] diff --git a/pubky-homeserver/src/persistence/sql/entities/session.rs b/pubky-homeserver/src/persistence/sql/entities/session.rs index 86eed8cf..daa05e4d 100644 --- a/pubky-homeserver/src/persistence/sql/entities/session.rs +++ b/pubky-homeserver/src/persistence/sql/entities/session.rs @@ -1,6 +1,6 @@ use std::{fmt::Display, str::FromStr}; -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use pubky_common::{capabilities::Capabilities, crypto::random_bytes, session::SessionInfo}; use sea_query::{Expr, Iden, PostgresQueryBuilder, Query, SimpleExpr}; use sea_query_binder::SqlxBinder; @@ -197,8 +197,8 @@ impl FromRow<'_, PgRow> for SessionEntity { #[cfg(test)] mod tests { - use pkarr::Keypair; use pubky_common::capabilities::Capability; + use pubky_common::crypto::Keypair; use crate::persistence::sql::{entities::user::UserRepository, SqlDb}; diff --git a/pubky-homeserver/src/persistence/sql/entities/signup_code.rs b/pubky-homeserver/src/persistence/sql/entities/signup_code.rs index 1cfe9298..0918ca4f 100644 --- a/pubky-homeserver/src/persistence/sql/entities/signup_code.rs +++ b/pubky-homeserver/src/persistence/sql/entities/signup_code.rs @@ -1,8 +1,8 @@ use std::{fmt::Display, str::FromStr}; use base32::{decode, encode, Alphabet}; -use pkarr::PublicKey; use pubky_common::crypto::random_bytes; +use pubky_common::crypto::PublicKey; use sea_query::{Expr, Iden, PostgresQueryBuilder, Query, SimpleExpr}; use sea_query_binder::SqlxBinder; use sqlx::{postgres::PgRow, FromRow, Row}; @@ -101,7 +101,7 @@ impl SignupCodeRepository { .table(SIGNUP_CODE_TABLE) .values(vec![( SignupCodeIden::UsedBy, - SimpleExpr::Value(used_by.to_string().into()), + SimpleExpr::Value(used_by.z32().into()), )]) .and_where(Expr::col(SignupCodeIden::Id).eq(id.to_string())) .returning_all() @@ -220,7 +220,7 @@ impl FromRow<'_, PgRow> for SignupCodeEntity { #[cfg(test)] mod tests { use crate::persistence::sql::SqlDb; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use super::*; diff --git a/pubky-homeserver/src/persistence/sql/entities/user.rs b/pubky-homeserver/src/persistence/sql/entities/user.rs index 66b8862f..6bac395d 100644 --- a/pubky-homeserver/src/persistence/sql/entities/user.rs +++ b/pubky-homeserver/src/persistence/sql/entities/user.rs @@ -1,4 +1,4 @@ -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use sea_query::{Expr, Iden, PostgresQueryBuilder, Query, SimpleExpr}; use sea_query_binder::SqlxBinder; use sqlx::{postgres::PgRow, FromRow, Row}; @@ -19,7 +19,7 @@ impl UserRepository { let statement = Query::insert() .into_table(USER_TABLE) .columns([UserIden::PublicKey]) - .values(vec![SimpleExpr::Value(public_key.to_string().into())]) + .values(vec![SimpleExpr::Value(public_key.z32().into())]) .unwrap() .returning_all() .to_owned(); @@ -46,7 +46,7 @@ impl UserRepository { UserIden::Disabled, UserIden::UsedBytes, ]) - .and_where(Expr::col(UserIden::PublicKey).eq(public_key.to_string())) + .and_where(Expr::col(UserIden::PublicKey).eq(public_key.z32())) .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); let con = executor.get_con().await?; @@ -62,7 +62,7 @@ impl UserRepository { let statement = Query::select() .from(USER_TABLE) .columns([UserIden::Id]) - .and_where(Expr::col(UserIden::PublicKey).eq(public_key.to_string())) + .and_where(Expr::col(UserIden::PublicKey).eq(public_key.z32())) .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); let con = executor.get_con().await?; @@ -235,7 +235,7 @@ impl FromRow<'_, PgRow> for UserEntity { #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use crate::persistence::sql::SqlDb; diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs b/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs index d37b461a..169580b4 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs @@ -75,12 +75,12 @@ enum User { #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use sea_query::{Query, SimpleExpr}; use sea_query_binder::SqlxBinder; use crate::persistence::sql::{migrator::Migrator, SqlDb}; - use pkarr::PublicKey; + use pubky_common::crypto::PublicKey; use sea_query::PostgresQueryBuilder; use sqlx::{postgres::PgRow, FromRow, Row}; @@ -131,7 +131,7 @@ mod tests { let statement = Query::insert() .into_table(USER_TABLE) .columns([User::PublicKey]) - .values(vec![SimpleExpr::Value(pubkey.to_string().into())]) + .values(vec![SimpleExpr::Value(pubkey.z32().into())]) .unwrap() .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs b/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs index cd6160fd..8d8f1423 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs @@ -50,7 +50,7 @@ enum SignupCodeIden { #[cfg(test)] mod tests { - use pkarr::{Keypair, PublicKey}; + use pubky_common::crypto::{Keypair, PublicKey}; use sea_query::{Query, SimpleExpr}; use sea_query_binder::SqlxBinder; use sqlx::{postgres::PgRow, FromRow, Row}; @@ -139,7 +139,7 @@ mod tests { .table(SIGNUP_CODE_TABLE) .values(vec![( SignupCodeIden::UsedBy, - SimpleExpr::Value(pubkey.to_string().into()), + SimpleExpr::Value(pubkey.z32().into()), )]) .and_where(Expr::col(SignupCodeIden::Id).eq(code.id)) .to_owned(); diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20250813_create_session.rs b/pubky-homeserver/src/persistence/sql/migrations/m20250813_create_session.rs index f1f67140..29a102dd 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20250813_create_session.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20250813_create_session.rs @@ -82,8 +82,8 @@ enum SessionIden { #[cfg(test)] mod tests { - use pkarr::Keypair; use pubky_common::capabilities::{Capabilities, Capability, CapsBuilder}; + use pubky_common::crypto::Keypair; use sea_query::{Query, SimpleExpr}; use sea_query_binder::SqlxBinder; use sqlx::{postgres::PgRow, FromRow, Row}; @@ -149,7 +149,7 @@ mod tests { let statement = Query::insert() .into_table(USERS_TABLE) .columns([UserIden::PublicKey]) - .values(vec![SimpleExpr::Value(pubkey.to_string().into())]) + .values(vec![SimpleExpr::Value(pubkey.z32().into())]) .unwrap() .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20250814_create_event.rs b/pubky-homeserver/src/persistence/sql/migrations/m20250814_create_event.rs index 19fe1b37..ba13fc40 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20250814_create_event.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20250814_create_event.rs @@ -66,7 +66,7 @@ enum EventIden { #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use sea_query::{Query, SimpleExpr}; use sea_query_binder::SqlxBinder; @@ -126,7 +126,7 @@ mod tests { let statement = Query::insert() .into_table(USERS_TABLE) .columns([UserIden::PublicKey]) - .values(vec![SimpleExpr::Value(pubkey.to_string().into())]) + .values(vec![SimpleExpr::Value(pubkey.z32().into())]) .unwrap() .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20250815_create_entry.rs b/pubky-homeserver/src/persistence/sql/migrations/m20250815_create_entry.rs index 6a2c8515..7a4f1cb7 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20250815_create_entry.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20250815_create_entry.rs @@ -98,7 +98,7 @@ enum EntryIden { #[cfg(test)] mod tests { - use pkarr::Keypair; + use pubky_common::crypto::Keypair; use sea_query::{Query, SimpleExpr}; use sea_query_binder::SqlxBinder; @@ -167,7 +167,7 @@ mod tests { let statement = Query::insert() .into_table(USERS_TABLE) .columns([UserIden::PublicKey]) - .values(vec![SimpleExpr::Value(pubkey.to_string().into())]) + .values(vec![SimpleExpr::Value(pubkey.z32().into())]) .unwrap() .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); @@ -247,7 +247,7 @@ mod tests { let statement = Query::insert() .into_table(USERS_TABLE) .columns([UserIden::PublicKey]) - .values(vec![SimpleExpr::Value(pubkey.to_string().into())]) + .values(vec![SimpleExpr::Value(pubkey.z32().into())]) .unwrap() .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20251014_events_table_index_and_content_hash.rs b/pubky-homeserver/src/persistence/sql/migrations/m20251014_events_table_index_and_content_hash.rs index 3bfc564c..1f303f1e 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20251014_events_table_index_and_content_hash.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20251014_events_table_index_and_content_hash.rs @@ -87,8 +87,8 @@ impl MigrationTrait for M20251014EventsTableIndexAndContentHashMigration { #[cfg(test)] mod tests { - use pkarr::Keypair; use pubky_common::crypto::Hash; + use pubky_common::crypto::Keypair; use sea_query::{Iden, Query, SimpleExpr}; use sea_query_binder::SqlxBinder; @@ -158,7 +158,7 @@ mod tests { let statement = Query::insert() .into_table(USERS_TABLE) .columns([UserIden::PublicKey]) - .values(vec![SimpleExpr::Value(pubkey.to_string().into())]) + .values(vec![SimpleExpr::Value(pubkey.z32().into())]) .unwrap() .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); @@ -303,7 +303,7 @@ mod tests { let statement = Query::insert() .into_table(USERS_TABLE) .columns([UserIden::PublicKey]) - .values(vec![SimpleExpr::Value(pubkey.to_string().into())]) + .values(vec![SimpleExpr::Value(pubkey.z32().into())]) .unwrap() .to_owned(); let (query, values) = statement.build_sqlx(PostgresQueryBuilder); diff --git a/pubky-homeserver/src/republishers/key_republisher.rs b/pubky-homeserver/src/republishers/key_republisher.rs index ee0818bb..de142aaa 100644 --- a/pubky-homeserver/src/republishers/key_republisher.rs +++ b/pubky-homeserver/src/republishers/key_republisher.rs @@ -183,7 +183,7 @@ mod tests { // Make sure the pkarr packet of the hs is resolvable. let _packet = pkarr_client.resolve(&hs_pubky).await.unwrap(); // Make sure the pkarr client can resolve the endpoint of the hs. - let qname = format!("{}", hs_pubky); + let qname = hs_pubky.z32(); let endpoint = pkarr_client .resolve_https_endpoint(qname.as_str()) .await @@ -198,7 +198,7 @@ mod tests { #[pubky_test_utils::test] async fn test_endpoints() { let mut context = AppContext::test().await; - context.keypair = pkarr::Keypair::random(); + context.keypair = pubky_common::crypto::Keypair::random(); let _republisher = HomeserverKeyRepublisher::start(&context, 8080, 8080) .await .unwrap(); diff --git a/pubky-homeserver/src/republishers/user_keys_republisher.rs b/pubky-homeserver/src/republishers/user_keys_republisher.rs index dc5a3b13..b3fa3474 100644 --- a/pubky-homeserver/src/republishers/user_keys_republisher.rs +++ b/pubky-homeserver/src/republishers/user_keys_republisher.rs @@ -1,9 +1,9 @@ use std::{collections::HashMap, time::Duration}; -use pkarr::PublicKey; use pkarr_republisher::{ MultiRepublishResult, MultiRepublisher, RepublisherSettings, ResilientClientBuilderError, }; +use pubky_common::crypto::PublicKey; use tokio::{ task::JoinHandle, time::{interval, Instant}, @@ -100,8 +100,10 @@ impl UserKeysRepublisher { settings.republish_condition(|_| true); let republisher = MultiRepublisher::new_with_settings(settings, Some(pkarr_builder)); // TODO: Only publish if user points to this home server. + let pkarr_keys: Vec = + keys.into_iter().map(pkarr::PublicKey::from).collect(); let results = republisher - .run(keys, 12) + .run(pkarr_keys, 12) .await .map_err(UserKeysRepublisherError::Pkarr)?; Ok(results) @@ -165,7 +167,7 @@ mod tests { use crate::persistence::sql::user::UserRepository; use crate::persistence::sql::SqlDb; use crate::republishers::user_keys_republisher::UserKeysRepublisher; - use pkarr::Keypair; + use pubky_common::crypto::Keypair; async fn init_db_with_users(count: usize) -> SqlDb { let db = SqlDb::test().await; diff --git a/pubky-homeserver/src/shared/pubkey_path_validator.rs b/pubky-homeserver/src/shared/pubkey_path_validator.rs index 92cf5b85..34c04085 100644 --- a/pubky-homeserver/src/shared/pubkey_path_validator.rs +++ b/pubky-homeserver/src/shared/pubkey_path_validator.rs @@ -1,4 +1,4 @@ -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; /// Custom validator for the zbase32 pubkey in the route path. /// Usage: diff --git a/pubky-homeserver/src/shared/webdav/entry_path.rs b/pubky-homeserver/src/shared/webdav/entry_path.rs index 655ef87a..2619cbfc 100644 --- a/pubky-homeserver/src/shared/webdav/entry_path.rs +++ b/pubky-homeserver/src/shared/webdav/entry_path.rs @@ -1,4 +1,4 @@ -use pkarr::PublicKey; +use pubky_common::crypto::PublicKey; use std::str::FromStr; use super::WebDavPath; @@ -30,7 +30,7 @@ pub struct EntryPath { impl EntryPath { pub fn new(pubkey: PublicKey, path: WebDavPath) -> Self { - let key = format!("{}{}", pubkey, path); + let key = format!("{}{}", pubkey.z32(), path); Self { pubkey, path, key } } @@ -110,7 +110,7 @@ mod tests { let pubkey = PublicKey::from_str("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo").unwrap(); let path = WebDavPath::new("/pub/folder/file.txt").unwrap(); - let key = format!("{pubkey}{path}"); + let key = format!("{}{path}", pubkey.z32()); let entry_path = EntryPath::new(pubkey, path); assert_eq!(entry_path.as_ref(), key); } diff --git a/pubky-sdk/README.md b/pubky-sdk/README.md index 8a4cb6d4..7a818294 100644 --- a/pubky-sdk/README.md +++ b/pubky-sdk/README.md @@ -250,7 +250,7 @@ let homeserver = testnet.homeserver_app(); let pubky = testnet.sdk()?; let signer = pubky.signer(Keypair::random()); -let session = signer.signup(&homeserver.public_key(), None).await?; +let session = signer.signup(&homeserver.public_key().into(), None).await?; session.storage().put("/pub/my-cool-app/hello.txt", "hi").await?; let s = session.storage().get("/pub/my-cool-app/hello.txt").await?.text().await?; diff --git a/pubky-sdk/bindings/js/src/wrappers/keys.rs b/pubky-sdk/bindings/js/src/wrappers/keys.rs index 4adb2519..415f49e0 100644 --- a/pubky-sdk/bindings/js/src/wrappers/keys.rs +++ b/pubky-sdk/bindings/js/src/wrappers/keys.rs @@ -2,16 +2,17 @@ use wasm_bindgen::prelude::*; use crate::js_error::JsResult; use js_sys::Uint8Array; +use pubky::{Keypair as NativeKeypair, PublicKey as NativePublicKey}; #[wasm_bindgen] -pub struct Keypair(pkarr::Keypair); +pub struct Keypair(NativeKeypair); #[wasm_bindgen] impl Keypair { #[wasm_bindgen] /// Generate a random [Keypair] pub fn random() -> Self { - Self(pkarr::Keypair::random()) + Self(NativeKeypair::random()) } /// Generate a [Keypair] from a secret key. @@ -21,13 +22,13 @@ impl Keypair { let secret: [u8; 32] = secret_key .try_into() .map_err(|_| format!("Expected secret_key to be 32 bytes, got {}", secret_len))?; - Ok(Self(pkarr::Keypair::from_secret_key(&secret))) + Ok(Self(NativeKeypair::from_secret_key(&secret))) } /// Returns the secret key of this keypair. #[wasm_bindgen(js_name = "secretKey")] pub fn secret_key(&self) -> Uint8Array { - Uint8Array::from(self.0.secret_key().as_ref()) + Uint8Array::from(self.0.as_inner().secret_key().as_ref()) } /// Returns the [PublicKey] of this keypair. @@ -44,7 +45,7 @@ impl Keypair { /// Create a recovery file for this keypair (encrypted with the given passphrase). #[wasm_bindgen(js_name = "createRecoveryFile")] pub fn create_recovery_file(&self, passphrase: &str) -> Uint8Array { - pubky_common::recovery_file::create_recovery_file(self.as_inner(), passphrase) + pubky_common::recovery_file::create_recovery_file(&self.0, passphrase) .as_slice() .into() } @@ -54,55 +55,63 @@ impl Keypair { pub fn from_recovery_file(recovery_file: &[u8], passphrase: &str) -> JsResult { let keypair = pubky_common::recovery_file::decrypt_recovery_file(recovery_file, passphrase)?; - Ok(Keypair::from(keypair)) + Ok(Keypair::from(NativeKeypair::from(keypair))) } } impl Keypair { - pub fn as_inner(&self) -> &pkarr::Keypair { + pub fn as_inner(&self) -> &NativeKeypair { &self.0 } } -impl From for Keypair { - fn from(keypair: pkarr::Keypair) -> Self { +impl From for Keypair { + fn from(keypair: NativeKeypair) -> Self { Self(keypair) } } #[wasm_bindgen] -pub struct PublicKey(pub(crate) pkarr::PublicKey); +pub struct PublicKey(pub(crate) NativePublicKey); #[wasm_bindgen] impl PublicKey { /// Convert the PublicKey to Uint8Array #[wasm_bindgen(js_name = "toUint8Array")] pub fn to_uint8array(&self) -> Uint8Array { - Uint8Array::from(self.0.as_bytes().as_ref()) + Uint8Array::from(self.0.as_inner().as_bytes().as_ref()) } #[wasm_bindgen] /// Returns the z-base32 encoding of this public key pub fn z32(&self) -> String { + self.0.z32() + } + + #[wasm_bindgen(js_name = "toString")] + /// Returns the identifier form with the `pubky` prefix. + pub fn to_string_js(&self) -> String { self.0.to_string() } #[wasm_bindgen(js_name = "from")] /// @throws pub fn try_from(value: String) -> JsResult { - let native_pk = pkarr::PublicKey::try_from(value)?; + let value = value.strip_prefix("pubky://").unwrap_or(&value); + let value = value.strip_prefix("pubky").unwrap_or(value); + let native_pk = NativePublicKey::try_from(value)?; Ok(PublicKey(native_pk)) } } impl PublicKey { - pub fn as_inner(&self) -> &pkarr::PublicKey { + pub fn as_inner(&self) -> &NativePublicKey { &self.0 } } -impl From for PublicKey { - fn from(value: pkarr::PublicKey) -> Self { +impl From for PublicKey { + fn from(value: NativePublicKey) -> Self { PublicKey(value) } } diff --git a/pubky-sdk/src/actors/pkdns.rs b/pubky-sdk/src/actors/pkdns.rs index 6ea5a43b..a53f97b0 100644 --- a/pubky-sdk/src/actors/pkdns.rs +++ b/pubky-sdk/src/actors/pkdns.rs @@ -8,12 +8,12 @@ use std::time::Duration; use pkarr::{ - Keypair, PublicKey, SignedPacket, Timestamp, + SignedPacket, Timestamp, dns::rdata::{RData, SVCB}, }; use crate::{ - PubkyHttpClient, PubkySigner, cross_log, + Keypair, PubkyHttpClient, PubkySigner, PublicKey, cross_log, errors::{AuthError, Error, PkarrError, Result}, }; @@ -440,7 +440,7 @@ mod tests { #[test] fn republish_preserves_non_pubky_records() { let keypair = Keypair::random(); - let original_host = Keypair::random().public_key().to_string(); + let original_host = Keypair::random().public_key().z32(); let mut dnslink_txt = TXT::new(); dnslink_txt @@ -467,7 +467,7 @@ mod tests { .sign(&keypair) .expect("signed existing packet"); - let new_host = Keypair::random().public_key().to_string(); + let new_host = Keypair::random().public_key().z32(); let republished = Pkdns::build_homeserver_packet(&keypair, &new_host, Some(&existing_packet)) @@ -481,13 +481,13 @@ mod tests { let original_dnslink = existing_packet .all_resource_records() .find(|rr| rr.name.to_string().starts_with("_dnslink")) - .map(|rr| rr.to_owned()) + .map(ToOwned::to_owned) .expect("original _dnslink record"); let republished_dnslink = republished .all_resource_records() .find(|rr| rr.name.to_string().starts_with("_dnslink")) - .map(|rr| rr.to_owned()) + .map(ToOwned::to_owned) .expect("republished _dnslink record"); assert_eq!(republished_dnslink.ttl, original_dnslink.ttl); diff --git a/pubky-sdk/src/actors/session/persist.rs b/pubky-sdk/src/actors/session/persist.rs index c1579bfc..e03ee895 100644 --- a/pubky-sdk/src/actors/session/persist.rs +++ b/pubky-sdk/src/actors/session/persist.rs @@ -1,6 +1,6 @@ use std::path::Path; -use pkarr::PublicKey; +use crate::PublicKey; use pubky_common::session::SessionInfo; use super::core::PubkySession; @@ -18,6 +18,7 @@ impl PubkySession { /// /// Treat the returned String as a **bearer secret**. Do not log it; store it /// securely. + #[must_use] pub fn export_secret(&self) -> String { let public_key = self.info().public_key().to_string(); let cookie = self.cookie.clone(); diff --git a/pubky-sdk/src/actors/signer/session.rs b/pubky-sdk/src/actors/signer/session.rs index 4a01e3e0..8441f183 100644 --- a/pubky-sdk/src/actors/signer/session.rs +++ b/pubky-sdk/src/actors/signer/session.rs @@ -120,7 +120,7 @@ impl PubkySigner { } fn build_signup_url(homeserver: &PublicKey, signup_token: Option<&str>) -> Result { - let mut url = Url::parse(&format!("https://{homeserver}"))?; + let mut url = Url::parse(&format!("https://{}", homeserver.z32()))?; url.set_path("/signup"); if let Some(token) = signup_token { url.query_pairs_mut().append_pair("signup_token", token); diff --git a/pubky-sdk/src/actors/storage/core.rs b/pubky-sdk/src/actors/storage/core.rs index 4f915501..1054cc1e 100644 --- a/pubky-sdk/src/actors/storage/core.rs +++ b/pubky-sdk/src/actors/storage/core.rs @@ -1,4 +1,4 @@ -use pkarr::PublicKey; +use crate::PublicKey; use reqwest::{Method, RequestBuilder}; use super::resource::{IntoPubkyResource, IntoResourcePath, PubkyResource, ResourcePath}; diff --git a/pubky-sdk/src/actors/storage/resource.rs b/pubky-sdk/src/actors/storage/resource.rs index ec187857..d1a9da0f 100644 --- a/pubky-sdk/src/actors/storage/resource.rs +++ b/pubky-sdk/src/actors/storage/resource.rs @@ -20,7 +20,7 @@ use std::{fmt, str::FromStr}; -use pkarr::PublicKey; +use crate::PublicKey; use url::Url; use crate::{Error, errors::RequestError}; @@ -165,16 +165,16 @@ impl fmt::Display for ResourcePath { /// /// ### Examples /// ``` -/// # use pkarr::Keypair; +/// # use pubky::Keypair; /// # use pubky::{PubkyResource, ResourcePath}; /// // Build from parts /// let pk = Keypair::random().public_key(); /// let r = PubkyResource::new(pk.clone(), "/pub/site/index.html")?; -/// assert_eq!(r.to_string(), format!("pubky{pk}/pub/site/index.html")); +/// assert_eq!(r.to_string(), format!("{pk}/pub/site/index.html", pk)); /// /// // Parse from string /// // `pubky://` form -/// let parsed2: PubkyResource = format!("pubky://{pk}/pub/site/index.html").parse()?; +/// let parsed2: PubkyResource = format!("pubky://{}/pub/site/index.html", pk.z32()).parse()?; /// /// # Ok::<(), pubky::Error>(()) /// ``` @@ -207,7 +207,7 @@ impl PubkyResource { #[must_use] pub fn to_pubky_url(&self) -> String { let rel = self.path.as_str().trim_start_matches('/'); - format!("pubky://{}/{}", self.owner, rel) + format!("pubky://{}/{}", self.owner.z32(), rel) } /// Render as `https://_pubky./` for transport. @@ -220,7 +220,7 @@ impl PubkyResource { /// - Returns [`Error::Request`] if the constructed transport URL is invalid. pub fn to_transport_url(&self) -> Result { let rel = self.path.as_str().trim_start_matches('/'); - let https = format!("https://_pubky.{}/{}", self.owner, rel); + let https = format!("https://_pubky.{}/{}", self.owner.z32(), rel); Ok(Url::parse(&https)?) } @@ -253,7 +253,7 @@ impl PubkyResource { /// Render as the identifier form `pubky/`. pub(crate) fn to_identifier(&self) -> String { let rel = self.path.as_str().trim_start_matches('/'); - format!("pubky{}/{}", self.owner, rel) + format!("{}/{}", self.owner.to_string(), rel) } } @@ -382,7 +382,7 @@ impl IntoResourcePath for &String { /// /// ### Examples /// ``` -/// # use pkarr::Keypair; +/// # use pubky::Keypair; /// # use pubky::{IntoPubkyResource, PubkyResource}; /// let user = Keypair::random().public_key(); /// @@ -390,7 +390,7 @@ impl IntoResourcePath for &String { /// let r1 = (user.clone(), "/pub/site/index.html").into_pubky_resource()?; /// /// // Parse `/` -/// let r2: PubkyResource = format!("pubky{}/pub/site/index.html", user).parse()?; +/// let r2: PubkyResource = format!("{}/pub/site/index.html", user).parse()?; /// /// // Parse `pubky://` /// let r3: PubkyResource = format!("pubky://{}/pub/site/index.html", user).parse()?; @@ -445,7 +445,7 @@ impl> IntoPubkyResource for (&PublicKey, P) { #[cfg(test)] mod tests { use super::*; - use pkarr::Keypair; + use crate::Keypair; #[test] fn file_path_normalization_and_rejections() { @@ -475,8 +475,9 @@ mod tests { fn parse_addressed_user_both_forms() { let kp = Keypair::random(); let user = kp.public_key(); - let s1 = format!("pubky://{}/pub/my-cool-app/file", user); - let s3 = format!("pubky{}/pub/my-cool-app/file", user); + let user_raw = user.z32(); + let s1 = format!("pubky://{user_raw}/pub/my-cool-app/file"); + let s3 = format!("pubky{user_raw}/pub/my-cool-app/file"); let p1 = PubkyResource::from_str(&s1).unwrap(); let p3 = PubkyResource::from_str(&s3).unwrap(); @@ -511,7 +512,7 @@ mod tests { let r = PubkyResource::new(user.clone(), p_abs.as_str()).unwrap(); assert_eq!( r.to_pubky_url(), - format!("pubky://{}/pub/my-cool-app/file", user) + format!("pubky://{}/pub/my-cool-app/file", user.z32()) ); } @@ -527,7 +528,7 @@ mod tests { )); // Double-slash inside path - let s_bad = format!("pubky{}/pub//app", user); + let s_bad = format!("{user}/pub//app"); assert!(matches!( PubkyResource::from_str(&s_bad), Err(Error::Request(RequestError::Validation { .. })) @@ -548,8 +549,8 @@ mod tests { #[test] fn rejects_dot_segments_but_allows_trailing_slash() { - assert!(ResourcePath::parse("/a/./b").is_err()); - assert!(ResourcePath::parse("/a/../b").is_err()); + ResourcePath::parse("/a/./b").unwrap_err(); + ResourcePath::parse("/a/../b").unwrap_err(); assert_eq!( ResourcePath::parse("/pub/my-cool-app/").unwrap().as_str(), "/pub/my-cool-app/" @@ -560,14 +561,14 @@ mod tests { fn resolve_identifiers() { let kp = Keypair::random(); let user = kp.public_key(); - let base = format!("pubky://{}/pub/site/index.html", user); + let base = format!("pubky://{}/pub/site/index.html", user.z32()); let resolved = resolve_pubky(&base).unwrap(); assert_eq!( resolved.as_str(), - format!("https://_pubky.{}/pub/site/index.html", user) + format!("https://_pubky.{}/pub/site/index.html", user.z32()) ); - let prefixed = format!("pubky{}/pub/site/index.html", user); + let prefixed = format!("pubky{}/pub/site/index.html", user.z32()); let resolved2 = resolve_pubky(&prefixed).unwrap(); assert_eq!(resolved, resolved2); @@ -577,7 +578,8 @@ mod tests { let parsed = PubkyResource::from_transport_url(&resolved).unwrap(); assert_eq!(parsed, resource); - let http_url = Url::parse(&format!("http://_pubky.{}/pub/site/index.html", user)).unwrap(); + let http_url = + Url::parse(&format!("http://_pubky.{}/pub/site/index.html", user.z32())).unwrap(); let parsed_http = PubkyResource::from_transport_url(&http_url).unwrap(); assert_eq!(parsed_http, resource); } diff --git a/pubky-sdk/src/client/http_targets/native.rs b/pubky-sdk/src/client/http_targets/native.rs index 4d8ba981..eb6cc9ee 100644 --- a/pubky-sdk/src/client/http_targets/native.rs +++ b/pubky-sdk/src/client/http_targets/native.rs @@ -50,7 +50,7 @@ impl PubkyHttpClient { /// the request body before sending. /// /// Differs from [`reqwest::Client::request`], in that it can make requests to: - /// 1. HTTPS URLs with a [`pkarr::PublicKey`] as top-level domain, by resolving + /// 1. HTTPS URLs with a [`crate::PublicKey`] as top-level domain, by resolving /// corresponding endpoints, and verifying TLS certificates accordingly. /// (example: `https://o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy`) /// 2. `_pubky.` URLs like `https://_pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy` diff --git a/pubky-sdk/src/client/http_targets/wasm.rs b/pubky-sdk/src/client/http_targets/wasm.rs index 41645db1..e4fd2108 100644 --- a/pubky-sdk/src/client/http_targets/wasm.rs +++ b/pubky-sdk/src/client/http_targets/wasm.rs @@ -1,9 +1,9 @@ //! HTTP methods that support `https://` with Pkarr domains, including `_pubky.` URLs +use crate::PublicKey; use crate::errors::{PkarrError, Result}; use crate::{PubkyHttpClient, cross_log}; use futures_lite::StreamExt; -use pkarr::PublicKey; use pkarr::extra::endpoints::Endpoint; use reqwest::{IntoUrl, Method, RequestBuilder}; use url::Url; @@ -158,8 +158,8 @@ impl PubkyHttpClient { #[cfg(all(test, target_arch = "wasm32"))] mod tests { use super::*; + use crate::Keypair; use futures_lite::stream; - use pkarr::Keypair; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -167,7 +167,7 @@ mod tests { #[wasm_bindgen_test(async)] async fn transform_url_errors_when_no_domain_is_found() { let client = PubkyHttpClient::new().unwrap(); - let pk = Keypair::random().public_key().to_string(); + let pk = Keypair::random().public_key().z32(); let mut url = Url::parse(&format!("https://_pubky.{pk}/pub/app/file.txt")).unwrap(); let original = url.clone(); diff --git a/pubky-sdk/src/lib.rs b/pubky-sdk/src/lib.rs index 6a2b04de..495b8282 100644 --- a/pubky-sdk/src/lib.rs +++ b/pubky-sdk/src/lib.rs @@ -61,11 +61,10 @@ pub use pkarr::DEFAULT_RELAYS; // Re-exports #[doc(inline)] -pub use pkarr::{Keypair, PublicKey}; -#[doc(inline)] pub use pubky_common::{ auth::AuthToken, capabilities::{Capabilities, Capability}, + crypto::{Keypair, PublicKey}, recovery_file, }; pub use reqwest::{Method, StatusCode}; diff --git a/pubky-sdk/src/pubky.rs b/pubky-sdk/src/pubky.rs index 25d63f45..86c4eacc 100644 --- a/pubky-sdk/src/pubky.rs +++ b/pubky-sdk/src/pubky.rs @@ -50,7 +50,7 @@ //! # Ok(()) } //! ``` -use pkarr::PublicKey; +use crate::PublicKey; use crate::{ Capabilities, Pkdns, PubkyAuthFlow, PubkyHttpClient, PubkySigner, PublicStorage, Result, diff --git a/pubky-testnet/src/ephemeral_testnet.rs b/pubky-testnet/src/ephemeral_testnet.rs index a21a5d31..bbae0be0 100644 --- a/pubky-testnet/src/ephemeral_testnet.rs +++ b/pubky-testnet/src/ephemeral_testnet.rs @@ -130,7 +130,7 @@ mod test { #[tokio::test] async fn test_homeserver_with_random_keypair() { let mut network = EphemeralTestnet::start_minimal().await.unwrap(); - assert!(network.testnet.homeservers.len() == 0); + assert!(network.testnet.homeservers.is_empty()); let _ = network.create_random_homeserver().await.unwrap(); let _ = network.create_random_homeserver().await.unwrap(); diff --git a/pubky-testnet/src/static_testnet.rs b/pubky-testnet/src/static_testnet.rs index 3a7dc41b..c615e1a6 100644 --- a/pubky-testnet/src/static_testnet.rs +++ b/pubky-testnet/src/static_testnet.rs @@ -190,7 +190,7 @@ impl StaticTestnet { } else { ConfigToml::test() }; - let keypair = pkarr::Keypair::from_secret_key(&[0; 32]); + let keypair = pubky_common::crypto::Keypair::from_secret_key(&[0; 32]); config.pkdns.dht_bootstrap_nodes = Some( self.bootstrap_nodes() .iter() diff --git a/pubky-testnet/src/testnet.rs b/pubky-testnet/src/testnet.rs index a1a701f3..b3d21473 100644 --- a/pubky-testnet/src/testnet.rs +++ b/pubky-testnet/src/testnet.rs @@ -292,7 +292,7 @@ mod test { let _packet = pkarr_client.resolve(&hs_pubky).await.unwrap(); // Make sure the pkarr can resolve the hs_pubky. - let pubkey = format!("{}", hs_pubky); + let pubkey = hs_pubky.z32(); let _endpoint = pkarr_client .resolve_https_endpoint(pubkey.as_str()) .await From b7a93e98b915238bfc9f30fcbe7f4defad5a9f43 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 27 Nov 2025 10:03:37 +0100 Subject: [PATCH 02/21] fix clippy --- pubky-sdk/src/actors/storage/resource.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky-sdk/src/actors/storage/resource.rs b/pubky-sdk/src/actors/storage/resource.rs index d1a9da0f..dd30c439 100644 --- a/pubky-sdk/src/actors/storage/resource.rs +++ b/pubky-sdk/src/actors/storage/resource.rs @@ -253,7 +253,7 @@ impl PubkyResource { /// Render as the identifier form `pubky/`. pub(crate) fn to_identifier(&self) -> String { let rel = self.path.as_str().trim_start_matches('/'); - format!("{}/{}", self.owner.to_string(), rel) + format!("{}/{}", self.owner, rel) } } From acb6c77d40ffb651ab432516068e5fbd879dd2c6 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 27 Nov 2025 12:16:27 +0100 Subject: [PATCH 03/21] fixes --- pubky-sdk/bindings/js/src/wrappers/keys.rs | 7 ++++--- pubky-sdk/src/actors/storage/resource.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pubky-sdk/bindings/js/src/wrappers/keys.rs b/pubky-sdk/bindings/js/src/wrappers/keys.rs index 415f49e0..6dde7989 100644 --- a/pubky-sdk/bindings/js/src/wrappers/keys.rs +++ b/pubky-sdk/bindings/js/src/wrappers/keys.rs @@ -33,10 +33,11 @@ impl Keypair { /// Returns the [PublicKey] of this keypair. /// - /// Use `.z32()` on the returned `PublicKey` to get the string form. + /// Use `.to_string()` on the returned `PublicKey` to get the string form. + /// or `.z32()` to get the z32 string form without prefix. /// /// @example - /// const who = keypair.publicKey.z32(); + /// const who = keypair.publicKey.to_string(); #[wasm_bindgen(js_name = "publicKey", getter)] pub fn public_key(&self) -> PublicKey { PublicKey(self.0.public_key()) @@ -55,7 +56,7 @@ impl Keypair { pub fn from_recovery_file(recovery_file: &[u8], passphrase: &str) -> JsResult { let keypair = pubky_common::recovery_file::decrypt_recovery_file(recovery_file, passphrase)?; - Ok(Keypair::from(NativeKeypair::from(keypair))) + Ok(Keypair::from(keypair)) } } diff --git a/pubky-sdk/src/actors/storage/resource.rs b/pubky-sdk/src/actors/storage/resource.rs index dd30c439..70706491 100644 --- a/pubky-sdk/src/actors/storage/resource.rs +++ b/pubky-sdk/src/actors/storage/resource.rs @@ -170,7 +170,7 @@ impl fmt::Display for ResourcePath { /// // Build from parts /// let pk = Keypair::random().public_key(); /// let r = PubkyResource::new(pk.clone(), "/pub/site/index.html")?; -/// assert_eq!(r.to_string(), format!("{pk}/pub/site/index.html", pk)); +/// assert_eq!(r.to_string(), format!("{pk}/pub/site/index.html")); /// /// // Parse from string /// // `pubky://` form From 2a51a79d70b8a893444f4a00ab3747dfa1d3a184 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 28 Nov 2025 12:09:37 +0100 Subject: [PATCH 04/21] fix e2e --- e2e/src/tests/events.rs | 172 ++++++++---------- e2e/src/tests/http.rs | 2 +- e2e/src/tests/rate_limiting.rs | 2 +- e2e/src/tests/storage.rs | 6 +- .../src/client_server/layers/authz.rs | 2 +- .../src/client_server/routes/auth.rs | 2 +- .../src/client_server/routes/events.rs | 11 +- .../src/client_server/routes/tenants/read.rs | 2 +- .../client_server/routes/tenants/session.rs | 2 +- .../sql/entities/entry/repository.rs | 7 +- pubky-sdk/bindings/js/src/client/http.rs | 2 +- pubky-sdk/src/actors/pkdns.rs | 2 +- pubky-sdk/src/actors/session/core.rs | 2 +- pubky-sdk/src/actors/session/persist.rs | 2 +- pubky-sdk/src/actors/storage/core.rs | 2 +- 15 files changed, 105 insertions(+), 113 deletions(-) diff --git a/e2e/src/tests/events.rs b/e2e/src/tests/events.rs index d3912452..7ccc9c82 100644 --- a/e2e/src/tests/events.rs +++ b/e2e/src/tests/events.rs @@ -21,6 +21,7 @@ async fn events_stream_basic_modes() { let testnet = EphemeralTestnet::start().await.unwrap(); let server = testnet.homeserver_app(); + let server_host = server.public_key().z32(); let pubky = testnet.sdk().unwrap(); // Create one user with 250 events - reuse for all subtests @@ -28,6 +29,7 @@ async fn events_stream_basic_modes() { let signer = pubky.signer(keypair); let session = signer.signup(&server.public_key(), None).await.unwrap(); let user_pubky = signer.public_key(); + let user_host = user_pubky.z32(); // Create 250 events (internal batch size is 100, tests pagination) for i in 0..250 { @@ -43,8 +45,7 @@ async fn events_stream_basic_modes() { // ==== Test 1: Historical auto-pagination (>100 events) ==== let stream_url = format!( "https://{}/events-stream?user={}&limit=255", - server.public_key(), - user_pubky + server_host, user_host ); let response = pubky .client() @@ -96,8 +97,7 @@ async fn events_stream_basic_modes() { // ==== Test 2: Finite limit enforcement ==== let stream_url = format!( "https://{}/events-stream?user={}&limit=50", - server.public_key(), - user_pubky + server_host, user_host ); let response = pubky .client() @@ -128,9 +128,7 @@ async fn events_stream_basic_modes() { // Reuse cursor_250 captured from Test 1 (event 250) let stream_url = format!( "https://{}/events-stream?user={}:{}&live=true", - server.public_key(), - user_pubky, - cursor_250 + server_host, user_host, cursor_250 ); let response = pubky .client() @@ -171,9 +169,7 @@ async fn events_stream_basic_modes() { // ==== Test 4: Live mode with limit - transitions from historical to live until limit reached ==== let stream_url = format!( "https://{}/events-stream?user={}:{}&live=true&limit=10", - server.public_key(), - user_pubky, - cursor_250 + server_host, user_host, cursor_250 ); let response = pubky .client() @@ -229,9 +225,7 @@ async fn events_stream_basic_modes() { // ==== Test 5: Batch mode (live=false) - connection closes after historical events ==== let stream_url = format!( "https://{}/events-stream?user={}:{}&limit=5", - server.public_key(), - user_pubky, - cursor_250 + server_host, user_host, cursor_250 ); let response = pubky .client() @@ -276,9 +270,7 @@ async fn events_stream_basic_modes() { // Fetch from cursor_250 which we know has 5 DEL events after it let stream_url = format!( "https://{}/events-stream?user={}:{}&limit=6", - server.public_key(), - user_pubky, - cursor_250 + server_host, user_host, cursor_250 ); let response = pubky .client() @@ -347,11 +339,11 @@ async fn events_stream_basic_modes() { .unwrap(); // Get the content_hash from HTTP GET headers (ETag) - let get_url = format!("https://{}/pub/hash_test.txt", server.public_key()); + let get_url = format!("https://{}/pub/hash_test.txt", server_host); let get_response = pubky .client() .request(Method::GET, &get_url) - .header("pubky-host", user_pubky.to_string()) + .header("pubky-host", user_host.to_string()) .send() .await .unwrap(); @@ -369,8 +361,7 @@ async fn events_stream_basic_modes() { // Get the content_hash from event stream let stream_url = format!( "https://{}/events-stream?user={}&path=/pub/hash_test.txt", - server.public_key(), - user_pubky + server_host, user_host ); let stream_response = pubky .client() @@ -412,8 +403,8 @@ async fn events_stream_basic_modes() { // Test 7a: Batch mode should close immediately let stream_url = format!( "https://{}/events-stream?user={}", - server.public_key(), - empty_user_pubky + server_host, + empty_user_pubky.z32() ); let response = pubky .client() @@ -432,8 +423,8 @@ async fn events_stream_basic_modes() { // Test 7b: Live mode should stay open and receive new events let stream_url = format!( "https://{}/events-stream?user={}&live=true", - server.public_key(), - empty_user_pubky + server_host, + empty_user_pubky.z32() ); let response = pubky .client() @@ -503,8 +494,8 @@ async fn events_stream_basic_modes() { // Test forward order first to establish baseline let stream_url = format!( "https://{}/events-stream?user={}&limit=5", - server.public_key(), - reverse_user_pubky + server_host, + reverse_user_pubky.z32() ); let response = pubky .client() @@ -532,8 +523,8 @@ async fn events_stream_basic_modes() { // Test reverse order let stream_url = format!( "https://{}/events-stream?user={}&reverse=true&limit=5", - server.public_key(), - reverse_user_pubky + server_host, + reverse_user_pubky.z32() ); let response = pubky .client() @@ -602,6 +593,7 @@ async fn events_stream_multiple_users() { let testnet = EphemeralTestnet::start().await.unwrap(); let server = testnet.homeserver_app(); + let server_host = server.public_key().z32(); let pubky = testnet.sdk().unwrap(); let keypair1 = Keypair::random(); @@ -638,9 +630,9 @@ async fn events_stream_multiple_users() { // Stream events for user1 and user2 (should get 5 events total) let stream_url = format!( "https://{}/events-stream?user={}&user={}", - server.public_key(), - pubky1, - pubky2 + server_host, + pubky1.z32(), + pubky2.z32() ); let response = pubky @@ -673,32 +665,23 @@ async fn events_stream_multiple_users() { // Verify we got events from both users assert_eq!(events.len(), 5, "Should receive 5 events total"); - let user1_events = events - .iter() - .filter(|e| e.contains(&pubky1.to_string())) - .count(); - let user2_events = events - .iter() - .filter(|e| e.contains(&pubky2.to_string())) - .count(); + let user1_events = events.iter().filter(|e| e.contains(&pubky1.z32())).count(); + let user2_events = events.iter().filter(|e| e.contains(&pubky2.z32())).count(); assert_eq!(user1_events, 3, "Should receive 3 events from user1"); assert_eq!(user2_events, 2, "Should receive 2 events from user2"); // Verify no events from user3 - let user3_events = events - .iter() - .filter(|e| e.contains(&pubky3.to_string())) - .count(); + let user3_events = events.iter().filter(|e| e.contains(&pubky3.z32())).count(); assert_eq!(user3_events, 0, "Should not receive events from user3"); // Now test that returned cursor values are correct with per-user cursors // Get the first 2 events and track cursor per user let stream_url_for_cursor = format!( "https://{}/events-stream?user={}&user={}&limit=2", - server.public_key(), - pubky1, - pubky2 + server_host, + pubky1.z32(), + pubky2.z32() ); let response = pubky @@ -744,18 +727,18 @@ async fn events_stream_multiple_users() { // Now request the remaining events using per-user cursors // This should properly handle the case where each user has a different cursor position // Build the URL conditionally based on whether we have cursors - let mut url_parts = vec![format!("https://{}/events-stream?", server.public_key())]; + let mut url_parts = vec![format!("https://{}/events-stream?", server_host)]; if !user1_cursor.is_empty() { - url_parts.push(format!("user={}:{}", pubky1, user1_cursor)); + url_parts.push(format!("user={}:{}", pubky1.z32(), user1_cursor)); } else { - url_parts.push(format!("user={}", pubky1)); + url_parts.push(format!("user={}", pubky1.z32())); } if !user2_cursor.is_empty() { - url_parts.push(format!("&user={}:{}", pubky2, user2_cursor)); + url_parts.push(format!("&user={}:{}", pubky2.z32(), user2_cursor)); } else { - url_parts.push(format!("&user={}", pubky2)); + url_parts.push(format!("&user={}", pubky2.z32())); } let stream_url_with_cursor = url_parts.join(""); @@ -792,11 +775,11 @@ async fn events_stream_multiple_users() { let user1_remaining = remaining_events .iter() - .filter(|e| e.contains(&pubky1.to_string())) + .filter(|e| e.contains(&pubky1.z32())) .count(); let user2_remaining = remaining_events .iter() - .filter(|e| e.contains(&pubky2.to_string())) + .filter(|e| e.contains(&pubky2.z32())) .count(); // With per-user cursors, each user's position is tracked independently: @@ -824,6 +807,7 @@ async fn events_stream_validation_errors() { let testnet = EphemeralTestnet::start().await.unwrap(); let server = testnet.homeserver_app(); + let server_host = server.public_key().z32(); let pubky = testnet.sdk().unwrap(); let keypair1 = Keypair::random(); @@ -838,7 +822,7 @@ async fn events_stream_validation_errors() { let invalid_pubkey = "invalid_key_not_zbase32"; // Test 1: No user parameter - let stream_url = format!("https://{}/events-stream", server.public_key()); + let stream_url = format!("https://{}/events-stream", server_host); let response = pubky .client() .request(Method::GET, &stream_url) @@ -857,11 +841,11 @@ async fn events_stream_validation_errors() { let mut query_params = vec![]; for _i in 0..51 { let keypair = Keypair::random(); - query_params.push(format!("user={}", keypair.public_key())); + query_params.push(format!("user={}", keypair.public_key().z32())); } let stream_url = format!( "https://{}/events-stream?{}", - server.public_key(), + server_host, query_params.join("&") ); let response = pubky @@ -877,8 +861,7 @@ async fn events_stream_validation_errors() { // Test 3: Invalid public key format let stream_url = format!( "https://{}/events-stream?user={}", - server.public_key(), - invalid_pubkey + server_host, invalid_pubkey ); let response = pubky .client() @@ -897,8 +880,8 @@ async fn events_stream_validation_errors() { // Test 4: Valid key but user not registered let stream_url = format!( "https://{}/events-stream?user={}", - server.public_key(), - pubky2 + server_host, + pubky2.z32() ); let response = pubky .client() @@ -915,9 +898,9 @@ async fn events_stream_validation_errors() { // Test 5: Mix of valid registered and unregistered user let stream_url = format!( "https://{}/events-stream?user={}&user={}", - server.public_key(), - pubky1, - pubky2 + server_host, + pubky1.z32(), + pubky2.z32() ); let response = pubky .client() @@ -934,8 +917,8 @@ async fn events_stream_validation_errors() { // Test 6: Mix of valid user and invalid key format let stream_url = format!( "https://{}/events-stream?user={}&user={}", - server.public_key(), - pubky1, + server_host, + pubky1.z32(), invalid_pubkey ); let response = pubky @@ -955,9 +938,7 @@ async fn events_stream_validation_errors() { // Test 7: Multiple invalid keys let stream_url = format!( "https://{}/events-stream?user={}&user={}", - server.public_key(), - invalid_pubkey, - "another_invalid_key" + server_host, invalid_pubkey, "another_invalid_key" ); let response = pubky .client() @@ -974,8 +955,8 @@ async fn events_stream_validation_errors() { // Test 8: Incompatible live=true with reverse=true let stream_url = format!( "https://{}/events-stream?user={}&live=true&reverse=true", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1002,8 +983,8 @@ async fn events_stream_validation_errors() { // Test 9a: Malformed cursor (non-numeric) let stream_url = format!( "https://{}/events-stream?user={}:abc123xyz", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1022,8 +1003,8 @@ async fn events_stream_validation_errors() { // Test 9b: Negative cursor (technically valid i64, but no events will have negative IDs) let stream_url = format!( "https://{}/events-stream?user={}:-100&limit=10", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1047,8 +1028,8 @@ async fn events_stream_validation_errors() { // Test 9c: Very large cursor beyond any events (should succeed but return no events) let stream_url = format!( "https://{}/events-stream?user={}:999999999&limit=10", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1072,8 +1053,8 @@ async fn events_stream_validation_errors() { // Test 10a: Path without leading slash - should automatically add "/" prefix let stream_url = format!( "https://{}/events-stream?user={}&path=pub/test.txt&limit=1", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1093,8 +1074,8 @@ async fn events_stream_validation_errors() { // Test 10b: Empty path parameter (should be treated as no filter) let stream_url = format!( "https://{}/events-stream?user={}&path=&limit=1", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1122,6 +1103,7 @@ async fn events_stream_path_filter() { let testnet = EphemeralTestnet::start().await.unwrap(); let server = testnet.homeserver_app(); let pubky = testnet.sdk().unwrap(); + let server_host = server.public_key().z32(); // Create 2 users upfront with diverse directory structures let keypair1 = Keypair::random(); @@ -1170,8 +1152,8 @@ async fn events_stream_path_filter() { // ==== Test 1: Basic filtering (both specific and broad paths) ==== let stream_url = format!( "https://{}/events-stream?user={}&path=/pub/files/", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1213,8 +1195,8 @@ async fn events_stream_path_filter() { // Get first 5 with cursor let stream_url = format!( "https://{}/events-stream?user={}&path=/pub/files/&limit=5", - server.public_key(), - pubky2 + server_host, + pubky2.z32() ); let response = pubky .client() @@ -1249,8 +1231,8 @@ async fn events_stream_path_filter() { // Get remaining 5 with cursor let stream_url = format!( "https://{}/events-stream?user={}:{}&path=/pub/files/", - server.public_key(), - pubky2, + server_host, + pubky2.z32(), cursor ); let response = pubky @@ -1286,9 +1268,9 @@ async fn events_stream_path_filter() { // Filter both users by files directory - user1 has 6 events (5 PUT + 1 DEL), user2 has 10 let stream_url = format!( "https://{}/events-stream?user={}&user={}&path=/pub/files/", - server.public_key(), - pubky1, - pubky2 + server_host, + pubky1.z32(), + pubky2.z32() ); let response = pubky .client() @@ -1319,11 +1301,11 @@ async fn events_stream_path_filter() { ); let user1_count = multi_events .iter() - .filter(|e| e.contains(&pubky1.to_string())) + .filter(|e| e.contains(&pubky1.z32())) .count(); let user2_count = multi_events .iter() - .filter(|e| e.contains(&pubky2.to_string())) + .filter(|e| e.contains(&pubky2.z32())) .count(); assert_eq!(user1_count, 6, "Multi-user: Should get 6 from user1"); assert_eq!(user2_count, 10, "Multi-user: Should get 10 from user2"); @@ -1332,8 +1314,8 @@ async fn events_stream_path_filter() { // Use user1's /pub/files/ which has 5 PUT + 1 DEL = 6 events let stream_url = format!( "https://{}/events-stream?user={}&path=/pub/files/&reverse=true&limit=6", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() @@ -1382,8 +1364,8 @@ async fn events_stream_path_filter() { let stream_url = format!( "https://{}/events-stream?user={}&path=/pub/my_folder/", - server.public_key(), - pubky1 + server_host, + pubky1.z32() ); let response = pubky .client() diff --git a/e2e/src/tests/http.rs b/e2e/src/tests/http.rs index 69c5a68f..9b1c02c4 100644 --- a/e2e/src/tests/http.rs +++ b/e2e/src/tests/http.rs @@ -9,7 +9,7 @@ async fn http_get_pubky() { let client = testnet.client().unwrap(); - let pubky_url = format!("https://{}/", server.public_key()); + let pubky_url = format!("https://{}/", server.public_key().z32()); let response = client .request(Method::GET, &pubky_url) .send() diff --git a/e2e/src/tests/rate_limiting.rs b/e2e/src/tests/rate_limiting.rs index c7e50775..ad8e7aa4 100644 --- a/e2e/src/tests/rate_limiting.rs +++ b/e2e/src/tests/rate_limiting.rs @@ -148,7 +148,7 @@ async fn test_limit_events() { let server = testnet.create_homeserver_app_with_mock(mock).await.unwrap(); // Events feed URL (pkarr host form) - let url = format!("https://{}/events/", server.public_key()); + let url = format!("https://{}/events/", server.public_key().z32()); // First request OK let res = client.request(Method::GET, &url).send().await.unwrap(); diff --git a/e2e/src/tests/storage.rs b/e2e/src/tests/storage.rs index 0dc13ced..bfc816d6 100644 --- a/e2e/src/tests/storage.rs +++ b/e2e/src/tests/storage.rs @@ -605,7 +605,7 @@ async fn list_events() { } // Feed is exposed under the public-key host - let feed_url = format!("https://{}/events/", server.public_key()); + let feed_url = format!("https://{}/events/", server.public_key().z32()); // Page 1 let cursor: String = { @@ -696,7 +696,7 @@ async fn read_after_event() { .unwrap(); // Events page 1 - let feed_url = format!("https://{}/events/", server.public_key()); + let feed_url = format!("https://{}/events/", server.public_key().z32()); { let page_url = format!("{feed_url}?limit=10"); let resp = pubky @@ -761,7 +761,7 @@ async fn dont_delete_shared_blobs() { assert_eq!(blob, file); // Event feed should show PUT u1, PUT u2, DEL u1 (order preserved) - let feed_url = format!("https://{}/events/", homeserver.public_key()); + let feed_url = format!("https://{}/events/", homeserver.public_key().z32()); let resp = pubky .client() .request(Method::GET, &feed_url) diff --git a/pubky-homeserver/src/client_server/layers/authz.rs b/pubky-homeserver/src/client_server/layers/authz.rs index 23013f72..bdbda21c 100644 --- a/pubky-homeserver/src/client_server/layers/authz.rs +++ b/pubky-homeserver/src/client_server/layers/authz.rs @@ -198,7 +198,7 @@ pub fn session_secret_from_cookies( public_key: &PublicKey, ) -> Option { let value = cookies - .get(&public_key.to_string()) + .get(&public_key.z32()) .map(|c| c.value().to_string())?; SessionSecret::new(value).ok() } diff --git a/pubky-homeserver/src/client_server/routes/auth.rs b/pubky-homeserver/src/client_server/routes/auth.rs index 43f808d0..56f4f277 100644 --- a/pubky-homeserver/src/client_server/routes/auth.rs +++ b/pubky-homeserver/src/client_server/routes/auth.rs @@ -137,7 +137,7 @@ async fn create_session_and_cookie( SessionRepository::create(user.id, capabilities, &mut state.sql_db.pool().into()).await?; // 3) Build and set cookie - let mut cookie = Cookie::new(user.public_key.to_string(), session_secret.to_string()); + let mut cookie = Cookie::new(user.public_key.z32(), session_secret.to_string()); configure_session_cookie(&mut cookie, host); // Set the cookie to expire in one year. let one_year = Duration::days(365); diff --git a/pubky-homeserver/src/client_server/routes/events.rs b/pubky-homeserver/src/client_server/routes/events.rs index 1292f6ec..dcb89b66 100644 --- a/pubky-homeserver/src/client_server/routes/events.rs +++ b/pubky-homeserver/src/client_server/routes/events.rs @@ -204,8 +204,15 @@ impl TryFrom for EventStreamQueryParams { /// data: cursor: 42 /// data: content_hash: r0NJufX5oaagQE3qNtzJSZvLJcmtwRK3zJqTyuQfMmI= (only for PUT events, base64-encoded blake3 hash) /// ``` +fn formatted_event_path(entity: &EventEntity) -> String { + // TODO: switch this formatter to use the shared `PubkyResource` type from `pubky-sdk` + // once the homeserver crate depends on it directly, so we avoid ad-hoc string + // reconstruction here. + format!("pubky://{}{}", entity.user_pubkey, entity.path.path()) +} + fn event_to_sse_data(entity: &EventEntity) -> String { - let path = format!("pubky://{}", entity.path.as_str()); + let path = formatted_event_path(entity); let cursor_line = format!("cursor: {}", entity.cursor()); let mut lines = vec![path, cursor_line]; @@ -255,7 +262,7 @@ pub async fn feed( .await?; let mut result = events .iter() - .map(|event| format!("{} pubky://{}", event.event_type, event.path.as_str())) + .map(|event| format!("{} {}", event.event_type, formatted_event_path(event))) .collect::>(); let next_cursor = events.last().map(|event| event.id.to_string()); diff --git a/pubky-homeserver/src/client_server/routes/tenants/read.rs b/pubky-homeserver/src/client_server/routes/tenants/read.rs index d6aa975f..fab4e043 100644 --- a/pubky-homeserver/src/client_server/routes/tenants/read.rs +++ b/pubky-homeserver/src/client_server/routes/tenants/read.rs @@ -254,7 +254,7 @@ mod tests { let body_bytes: axum::body::Bytes = auth_token.serialize().into(); let response = server .post("/signup") - .add_header("host", keypair.public_key().to_string()) + .add_header("host", keypair.public_key().to_z32()) .bytes(body_bytes) .expect_success() .await; diff --git a/pubky-homeserver/src/client_server/routes/tenants/session.rs b/pubky-homeserver/src/client_server/routes/tenants/session.rs index f6cefcdc..b557bfe7 100644 --- a/pubky-homeserver/src/client_server/routes/tenants/session.rs +++ b/pubky-homeserver/src/client_server/routes/tenants/session.rs @@ -58,7 +58,7 @@ pub async fn signout( // Always instruct the client to drop the session cookie, even if the // database record was already gone. This keeps repeated signout calls // idempotent and lets browsers wipe stale cookies immediately. - let mut removal = Cookie::new(pubky.public_key().to_string(), String::new()); + let mut removal = Cookie::new(pubky.public_key().z32(), String::new()); removal.make_removal(); configure_session_cookie(&mut removal, &host); cookies.add(removal); diff --git a/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs b/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs index f18dba3c..0e2ed326 100644 --- a/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs +++ b/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs @@ -338,11 +338,14 @@ impl EntryRepository { if reverse { statement = statement - .order_by((ENTRY_TABLE, EntryIden::Path), Order::Desc) + .order_by_expr( + Expr::col((ENTRY_TABLE, EntryIden::Path)).into(), + Order::Desc, + ) .to_owned(); } else { statement = statement - .order_by((ENTRY_TABLE, EntryIden::Path), Order::Asc) + .order_by_expr(Expr::col((ENTRY_TABLE, EntryIden::Path)).into(), Order::Asc) .to_owned(); } diff --git a/pubky-sdk/bindings/js/src/client/http.rs b/pubky-sdk/bindings/js/src/client/http.rs index 4c8754ba..0e7370ce 100644 --- a/pubky-sdk/bindings/js/src/client/http.rs +++ b/pubky-sdk/bindings/js/src/client/http.rs @@ -170,7 +170,7 @@ mod tests { let client = Client::testnet(None).unwrap(); let keypair = Keypair::random(); seed_pkarr_testnet_endpoint(&client, &keypair, "localhost", 15411); - let pk = keypair.public_key().to_string(); + let pk = keypair.public_key().z32(); let mut url = Url::parse(&format!("https://_pubky.{}/pub/file.txt", pk)).unwrap(); let err = client.0.prepare_request(&mut url).await.unwrap_err(); diff --git a/pubky-sdk/src/actors/pkdns.rs b/pubky-sdk/src/actors/pkdns.rs index a53f97b0..e0ba05a2 100644 --- a/pubky-sdk/src/actors/pkdns.rs +++ b/pubky-sdk/src/actors/pkdns.rs @@ -415,7 +415,7 @@ fn determine_host( ) -> Option { if let Some(host) = override_host { cross_log!(info, "Using override host {} for `_pubky` publish", host); - return Some(host.to_string()); + return Some(host.z32()); } cross_log!(debug, "Deriving publish host from existing `_pubky` record"); dht_packet.and_then(extract_host_from_packet) diff --git a/pubky-sdk/src/actors/session/core.rs b/pubky-sdk/src/actors/session/core.rs index 944e6128..e136b55f 100644 --- a/pubky-sdk/src/actors/session/core.rs +++ b/pubky-sdk/src/actors/session/core.rs @@ -102,7 +102,7 @@ impl PubkySession { let info = SessionInfo::deserialize(&bytes)?; // 3) Find the cookie named exactly as the user's pubky. - let cookie_name = info.public_key().to_string(); + let cookie_name = info.public_key().z32(); let cookie = raw_set_cookies .iter() .filter_map(|raw| cookie::Cookie::parse(raw.clone()).ok()) diff --git a/pubky-sdk/src/actors/session/persist.rs b/pubky-sdk/src/actors/session/persist.rs index e03ee895..ebfa7a2d 100644 --- a/pubky-sdk/src/actors/session/persist.rs +++ b/pubky-sdk/src/actors/session/persist.rs @@ -20,7 +20,7 @@ impl PubkySession { /// securely. #[must_use] pub fn export_secret(&self) -> String { - let public_key = self.info().public_key().to_string(); + let public_key = self.info().public_key().z32(); let cookie = self.cookie.clone(); cross_log!(info, "Exporting session secret for {}", public_key); format!("{public_key}:{cookie}") diff --git a/pubky-sdk/src/actors/storage/core.rs b/pubky-sdk/src/actors/storage/core.rs index 1054cc1e..5478bef6 100644 --- a/pubky-sdk/src/actors/storage/core.rs +++ b/pubky-sdk/src/actors/storage/core.rs @@ -67,7 +67,7 @@ impl SessionStorage { #[cfg(not(target_arch = "wasm32"))] pub(crate) fn with_session_cookie(&self, rb: RequestBuilder) -> RequestBuilder { - let cookie_name = self.user.to_string(); + let cookie_name = self.user.z32(); rb.header( reqwest::header::COOKIE, format!("{cookie_name}={}", self.cookie), From b563ea4219abcf06696b6ed279dd1d006f1e7c91 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 28 Nov 2025 13:45:44 +0100 Subject: [PATCH 05/21] fix wasm tests --- pubky-sdk/bindings/js/src/client/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky-sdk/bindings/js/src/client/http.rs b/pubky-sdk/bindings/js/src/client/http.rs index 0e7370ce..806b2ad1 100644 --- a/pubky-sdk/bindings/js/src/client/http.rs +++ b/pubky-sdk/bindings/js/src/client/http.rs @@ -170,7 +170,7 @@ mod tests { let client = Client::testnet(None).unwrap(); let keypair = Keypair::random(); seed_pkarr_testnet_endpoint(&client, &keypair, "localhost", 15411); - let pk = keypair.public_key().z32(); + let pk = keypair.public_key().to_z32(); let mut url = Url::parse(&format!("https://_pubky.{}/pub/file.txt", pk)).unwrap(); let err = client.0.prepare_request(&mut url).await.unwrap_err(); From f6b5970e03e7f2bfb35b3f6179022b4a2228c2a3 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Mon, 15 Dec 2025 09:05:38 +0100 Subject: [PATCH 06/21] add secret export method to keypair wrapper --- pubky-common/src/crypto/keys.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pubky-common/src/crypto/keys.rs b/pubky-common/src/crypto/keys.rs index 97279ef0..780fd011 100644 --- a/pubky-common/src/crypto/keys.rs +++ b/pubky-common/src/crypto/keys.rs @@ -24,6 +24,14 @@ impl Keypair { Self(pkarr::Keypair::random()) } + /// Export the secret key bytes. + #[must_use] + pub fn secret_key(&self) -> [u8; 32] { + let mut out = [0u8; 32]; + out.copy_from_slice(self.0.secret_key().as_ref()); + out + } + /// Construct a [`Keypair`] from a 32-byte secret key. #[must_use] pub fn from_secret_key(secret: &[u8; 32]) -> Self { From 15fd7ff459e321db8c4ca54dbc84f6ca8fd03bb4 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 2 Jan 2026 09:26:40 +0100 Subject: [PATCH 07/21] fmt --- pubky-sdk/src/actors/auth/auth_flow.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pubky-sdk/src/actors/auth/auth_flow.rs b/pubky-sdk/src/actors/auth/auth_flow.rs index fdace7f1..4b2475fe 100644 --- a/pubky-sdk/src/actors/auth/auth_flow.rs +++ b/pubky-sdk/src/actors/auth/auth_flow.rs @@ -65,8 +65,7 @@ use url::Url; use pubky_common::crypto::random_bytes; use crate::{ - AuthToken, Capabilities, PubkyHttpClient, PubkySession, - PublicKey, + AuthToken, Capabilities, PubkyHttpClient, PubkySession, PublicKey, actors::{ DEFAULT_HTTP_RELAY, auth::{ From cc55c1ea62837ffb6122af8c3c57f84bfd3f331d Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 2 Jan 2026 09:39:32 +0100 Subject: [PATCH 08/21] fix clippy --- e2e/src/tests/rate_limiting.rs | 6 +++-- e2e/src/tests/storage.rs | 20 ++++++++++++---- http-relay/src/http_relay.rs | 6 +++-- .../examples/publish_and_save.rs | 17 +++++++------- .../src/actors/auth/auth_subscription.rs | 4 ++-- .../src/actors/auth/deep_links/seed_export.rs | 4 ++-- .../src/actors/auth/deep_links/signin.rs | 4 ++-- .../src/actors/auth/deep_links/signup.rs | 8 +++---- .../actors/auth/http_relay_link_channel.rs | 23 +++++++++---------- pubky-sdk/src/actors/session/core.rs | 7 ++++++ pubky-sdk/src/client/http_targets/native.rs | 7 ++++++ test_utils/test_macro/src/lib.rs | 2 +- 12 files changed, 68 insertions(+), 40 deletions(-) diff --git a/e2e/src/tests/rate_limiting.rs b/e2e/src/tests/rate_limiting.rs index ad8e7aa4..aab2d1e9 100644 --- a/e2e/src/tests/rate_limiting.rs +++ b/e2e/src/tests/rate_limiting.rs @@ -243,7 +243,8 @@ async fn test_concurrent_write_read() { let start = Instant::now(); { let mut tasks = Vec::with_capacity(user_count); - for session in sessions.iter().cloned() { + for session in &sessions { + let session = session.clone(); let body = body.clone(); tasks.push(tokio::spawn(async move { session.storage().put(path, body).await.unwrap(); @@ -265,7 +266,8 @@ async fn test_concurrent_write_read() { let start = Instant::now(); { let mut tasks = Vec::with_capacity(user_count); - for session in sessions.iter().cloned() { + for session in &sessions { + let session = session.clone(); tasks.push(tokio::spawn(async move { let resp = session.storage().get(path).await.unwrap(); let _ = resp.bytes().await.unwrap(); // read body to apply full 3 KB download diff --git a/e2e/src/tests/storage.rs b/e2e/src/tests/storage.rs index bfc816d6..ab7392ab 100644 --- a/e2e/src/tests/storage.rs +++ b/e2e/src/tests/storage.rs @@ -309,7 +309,7 @@ async fn list_deep() { } // List all files with no cursor, no limit - let url = format!("/pub/example.com/"); + let url = "/pub/example.com/".to_string(); { let list = owner_session .storage() @@ -448,7 +448,7 @@ async fn list_shallow() { } // List all files with no cursor, no limit - let url = format!("/pub/"); + let url = "/pub/".to_string(); { let list = owner_session .storage() @@ -621,7 +621,13 @@ async fn list_events() { let lines = text.split('\n').collect::>(); // last line is "cursor: " - let cursor = lines.last().unwrap().split(' ').last().unwrap().to_string(); + let cursor = lines + .last() + .unwrap() + .rsplit(' ') + .next() + .unwrap() + .to_string(); assert_eq!( lines, @@ -708,7 +714,13 @@ async fn read_after_event() { let text = resp.text().await.unwrap(); let lines = text.split('\n').collect::>(); - let cursor = lines.last().unwrap().split(' ').last().unwrap().to_string(); + let cursor = lines + .last() + .unwrap() + .rsplit(' ') + .next() + .unwrap() + .to_string(); assert_eq!( lines, diff --git a/http-relay/src/http_relay.rs b/http-relay/src/http_relay.rs index 2b5efdd8..52d50c9b 100644 --- a/http-relay/src/http_relay.rs +++ b/http-relay/src/http_relay.rs @@ -282,8 +282,10 @@ mod tests { #[tokio::test] async fn test_request_timeout() { - let mut config = Config::default(); - config.request_timeout = Duration::from_millis(50); + let config = Config { + request_timeout: Duration::from_millis(50), + ..Config::default() + }; let (app, state) = HttpRelay::create_app(config).unwrap(); let server = axum_test::TestServer::new(app).unwrap(); diff --git a/pkarr-republisher/examples/publish_and_save.rs b/pkarr-republisher/examples/publish_and_save.rs index 3c22b9cd..850e5c02 100644 --- a/pkarr-republisher/examples/publish_and_save.rs +++ b/pkarr-republisher/examples/publish_and_save.rs @@ -140,16 +140,15 @@ async fn publish_records(num_records: usize, thread_id: usize) -> Vec { .cname(Name::new("test").unwrap(), Name::new("test2").unwrap(), 600) .build(&key) .unwrap(); - let result = rclient.publish(packet, None).await; let elapsed_time = instant.elapsed().as_millis(); - if result.is_ok() { - let info = result.unwrap(); - tracing::info!("- t{thread_id:<2} {i:>3}/{num_records} Published {} within {elapsed_time}ms to {} nodes {} attempts", key.public_key(), info.published_nodes_count, info.attempts_needed); - records.push(key); - } else { - let e = result.unwrap_err(); - tracing::error!("Failed to publish {} record: {e:?}", key.public_key()); - continue; + match rclient.publish(packet, None).await { + Ok(info) => { + tracing::info!("- t{thread_id:<2} {i:>3}/{num_records} Published {} within {elapsed_time}ms to {} nodes {} attempts", key.public_key(), info.published_nodes_count, info.attempts_needed); + records.push(key); + } + Err(e) => { + tracing::error!("Failed to publish {} record: {e:?}", key.public_key()); + } } } records diff --git a/pubky-sdk/src/actors/auth/auth_subscription.rs b/pubky-sdk/src/actors/auth/auth_subscription.rs index a42f8d68..167da9be 100644 --- a/pubky-sdk/src/actors/auth/auth_subscription.rs +++ b/pubky-sdk/src/actors/auth/auth_subscription.rs @@ -248,7 +248,7 @@ mod tests { }); let (producer_result, poll_result) = tokio::join!(producer_handle, poll_handle); - assert!(producer_result.is_ok()); - assert!(poll_result.is_ok()); + producer_result.unwrap(); + poll_result.unwrap(); } } diff --git a/pubky-sdk/src/actors/auth/deep_links/seed_export.rs b/pubky-sdk/src/actors/auth/deep_links/seed_export.rs index 0bc85598..7cb3d98f 100644 --- a/pubky-sdk/src/actors/auth/deep_links/seed_export.rs +++ b/pubky-sdk/src/actors/auth/deep_links/seed_export.rs @@ -89,13 +89,13 @@ mod tests { fn test_signin_deep_link_parse() { let keypair = Keypair::random(); let secret = keypair.secret_key(); - let deep_link = SeedExportDeepLink::new(secret.clone()); + let deep_link = SeedExportDeepLink::new(secret); let deep_link_str = deep_link.to_string(); assert_eq!( deep_link_str, format!( "pubkyauth://secret_export?secret={}", - URL_SAFE_NO_PAD.encode(&secret) + URL_SAFE_NO_PAD.encode(secret) ) ); let deep_link_parsed = SeedExportDeepLink::from_str(&deep_link_str).unwrap(); diff --git a/pubky-sdk/src/actors/auth/deep_links/signin.rs b/pubky-sdk/src/actors/auth/deep_links/signin.rs index 1bfb099a..e7f2fd69 100644 --- a/pubky-sdk/src/actors/auth/deep_links/signin.rs +++ b/pubky-sdk/src/actors/auth/deep_links/signin.rs @@ -139,7 +139,7 @@ mod tests { .finish(); let relay = Url::parse("https://httprelay.pubky.app/link/").unwrap(); let secret = [123; 32]; - let deep_link = SigninDeepLink::new(capabilities.clone(), relay.clone(), secret.clone()); + let deep_link = SigninDeepLink::new(capabilities.clone(), relay.clone(), secret); let deep_link_str = deep_link.to_string(); assert_eq!( deep_link_str, @@ -147,7 +147,7 @@ mod tests { "pubkyauth://signin?caps={}&relay={}&secret={}", capabilities, relay, - URL_SAFE_NO_PAD.encode(&secret) + URL_SAFE_NO_PAD.encode(secret) ) ); let deep_link_parsed = SigninDeepLink::from_str(&deep_link_str).unwrap(); diff --git a/pubky-sdk/src/actors/auth/deep_links/signup.rs b/pubky-sdk/src/actors/auth/deep_links/signup.rs index eb99d0e7..4a64268a 100644 --- a/pubky-sdk/src/actors/auth/deep_links/signup.rs +++ b/pubky-sdk/src/actors/auth/deep_links/signup.rs @@ -187,7 +187,7 @@ mod tests { let deep_link = SignupDeepLink::new( capabilities.clone(), relay.clone(), - secret.clone(), + secret, homeserver.clone(), None, ); @@ -198,7 +198,7 @@ mod tests { "pubkyauth://signup?caps={}&relay={}&secret={}&hs={}", capabilities, relay, - URL_SAFE_NO_PAD.encode(&secret), + URL_SAFE_NO_PAD.encode(secret), homeserver.z32() ) ); @@ -220,7 +220,7 @@ mod tests { let deep_link = SignupDeepLink::new( capabilities.clone(), relay.clone(), - secret.clone(), + secret, homeserver.clone(), Some(signup_token.to_string()), ); @@ -231,7 +231,7 @@ mod tests { "pubkyauth://signup?caps={}&relay={}&secret={}&hs={}&st={}", capabilities, relay, - URL_SAFE_NO_PAD.encode(&secret), + URL_SAFE_NO_PAD.encode(secret), homeserver.z32(), signup_token ) diff --git a/pubky-sdk/src/actors/auth/http_relay_link_channel.rs b/pubky-sdk/src/actors/auth/http_relay_link_channel.rs index 210b6a6c..9c434b82 100644 --- a/pubky-sdk/src/actors/auth/http_relay_link_channel.rs +++ b/pubky-sdk/src/actors/auth/http_relay_link_channel.rs @@ -302,17 +302,16 @@ mod tests { url::ParseError::RelativeUrlWithCannotBeABaseBase ) ), - "Expected MissingChannelId error, got {:?}", - e + "Expected MissingChannelId error, got {e:?}" ); } - }; + } } fn random_channel_url() -> String { let channel_bytes = random_bytes::<32>(); - let channel_id = URL_SAFE_NO_PAD.encode(&channel_bytes); - format!("{}/link/{}", DEFAULT_HTTP_RELAY, channel_id) + let channel_id = URL_SAFE_NO_PAD.encode(channel_bytes); + format!("{DEFAULT_HTTP_RELAY}/link/{channel_id}") } #[tokio::test] @@ -336,8 +335,8 @@ mod tests { }); let (poll_result, produce_result) = tokio::join!(poll_handle, produce_handle); - assert!(poll_result.is_ok()); - assert!(produce_result.is_ok()); + poll_result.unwrap(); + produce_result.unwrap(); } /// Test that a poll can time out and then resume successfully. @@ -358,7 +357,7 @@ mod tests { Err(e) => { assert!(matches!(e, PollError::Timeout)); } - }; + } // Try again and should succeed let response = channel.poll_once(&client, None).await.unwrap(); @@ -378,8 +377,8 @@ mod tests { }); let (poll_result, produce_result) = tokio::join!(poll_handle, produce_handle); - assert!(poll_result.is_ok()); - assert!(produce_result.is_ok()); + poll_result.unwrap(); + produce_result.unwrap(); } #[tokio::test] @@ -402,7 +401,7 @@ mod tests { }); let (produce_result, poll_result) = tokio::join!(produce_handle, poll_handle); - assert!(produce_result.is_ok()); - assert!(poll_result.is_ok()); + produce_result.unwrap(); + poll_result.unwrap(); } } diff --git a/pubky-sdk/src/actors/session/core.rs b/pubky-sdk/src/actors/session/core.rs index 30f48359..6e3281a2 100644 --- a/pubky-sdk/src/actors/session/core.rs +++ b/pubky-sdk/src/actors/session/core.rs @@ -255,7 +255,14 @@ impl PubkySession { /// Restore a session from an `export()` string (unsupported on native targets). /// /// Use [`Self::import_secret`] on native to restore a session using the secret token instead. + /// + /// # Errors + /// - Returns [`crate::errors::RequestError::Validation`] because exports are only supported on WASM. #[cfg(not(target_arch = "wasm32"))] + #[allow( + clippy::unused_async, + reason = "keep async signature aligned with WASM build" + )] pub async fn import(_export: &str, _client: Option) -> Result { Err(RequestError::Validation { message: "session import is only supported on WASM targets".into(), diff --git a/pubky-sdk/src/client/http_targets/native.rs b/pubky-sdk/src/client/http_targets/native.rs index 86fc9b88..3600ec3d 100644 --- a/pubky-sdk/src/client/http_targets/native.rs +++ b/pubky-sdk/src/client/http_targets/native.rs @@ -48,6 +48,13 @@ impl PubkyHttpClient { /// /// Native builds do not rewrite URLs; we only detect pubky hosts and return the /// `pubky-host` value when applicable. + /// + /// # Errors + /// - This function does not currently return errors; it keeps the `Result` to match WASM. + #[allow( + clippy::unused_async, + reason = "keep async signature aligned with WASM build" + )] pub async fn prepare_request(&self, url: &mut Url) -> Result> { let host = url.host_str().unwrap_or(""); diff --git a/test_utils/test_macro/src/lib.rs b/test_utils/test_macro/src/lib.rs index 02a1ffef..4de540e3 100644 --- a/test_utils/test_macro/src/lib.rs +++ b/test_utils/test_macro/src/lib.rs @@ -130,6 +130,6 @@ mod tests { fn macro_compiles() { // This test just ensures the macro compiles correctly // The actual functionality is tested in integration tests - assert!(true); + let _ = (); } } From c8766c74cba34a065a6d485e53608642295ce43e Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 2 Jan 2026 10:32:47 +0100 Subject: [PATCH 09/21] tigten prefix handling --- pubky-common/src/crypto/keys.rs | 5 ++++- pubky-homeserver/src/client_server/app.rs | 2 +- pubky-homeserver/src/homeserver_app.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pubky-common/src/crypto/keys.rs b/pubky-common/src/crypto/keys.rs index 780fd011..a9a04ba9 100644 --- a/pubky-common/src/crypto/keys.rs +++ b/pubky-common/src/crypto/keys.rs @@ -9,7 +9,10 @@ use serde::{Deserialize, Serialize}; type ParseError = >::Error; fn parse_public_key(value: &str) -> Result { - let raw = value.strip_prefix("pubky").unwrap_or(value); + let raw = match value.strip_prefix("pubky") { + Some(stripped) if stripped.len() == 52 => stripped, + _ => value, + }; pkarr::PublicKey::try_from(raw.to_string()) } diff --git a/pubky-homeserver/src/client_server/app.rs b/pubky-homeserver/src/client_server/app.rs index 5f6ea95b..150c0f04 100644 --- a/pubky-homeserver/src/client_server/app.rs +++ b/pubky-homeserver/src/client_server/app.rs @@ -184,7 +184,7 @@ impl ClientServer { /// Get the URL of the pubky tls server with the Pubky DNS name. pub fn pubky_tls_dns_url_string(&self) -> String { - format!("https://{}", self.context.keypair.public_key()) + format!("https://{}", self.context.keypair.public_key().z32()) } /// Get the URL of the pubky tls server with the Pubky IP address. diff --git a/pubky-homeserver/src/homeserver_app.rs b/pubky-homeserver/src/homeserver_app.rs index 25c4abdf..1c25d69e 100644 --- a/pubky-homeserver/src/homeserver_app.rs +++ b/pubky-homeserver/src/homeserver_app.rs @@ -137,7 +137,7 @@ impl HomeserverApp { /// Returns the `https://` url pub fn pubky_url(&self) -> url::Url { - url::Url::parse(&format!("https://{}", self.public_key())).expect("valid url") + url::Url::parse(&format!("https://{}", self.public_key().z32())).expect("valid url") } /// Returns the `https://` url From 11eb9ace5b764ee271da5945ccbef68ebafbb012 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 2 Jan 2026 13:14:00 +0100 Subject: [PATCH 10/21] tighten wasm public key conversion --- pubky-sdk/bindings/js/src/wrappers/keys.rs | 5 +++- pubky-sdk/src/client/http_targets/wasm.rs | 28 ++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/pubky-sdk/bindings/js/src/wrappers/keys.rs b/pubky-sdk/bindings/js/src/wrappers/keys.rs index b540661e..233b2944 100644 --- a/pubky-sdk/bindings/js/src/wrappers/keys.rs +++ b/pubky-sdk/bindings/js/src/wrappers/keys.rs @@ -100,7 +100,10 @@ impl PublicKey { /// @throws pub fn try_from(value: String) -> JsResult { let value = value.strip_prefix("pubky://").unwrap_or(&value); - let value = value.strip_prefix("pubky").unwrap_or(value); + let value = match value.strip_prefix("pubky") { + Some(stripped) if stripped.len() == 52 => stripped, + _ => value, + }; let native_pk = NativePublicKey::try_from(value)?; Ok(PublicKey(native_pk)) } diff --git a/pubky-sdk/src/client/http_targets/wasm.rs b/pubky-sdk/src/client/http_targets/wasm.rs index e4fd2108..c6e1d337 100644 --- a/pubky-sdk/src/client/http_targets/wasm.rs +++ b/pubky-sdk/src/client/http_targets/wasm.rs @@ -1,7 +1,7 @@ //! HTTP methods that support `https://` with Pkarr domains, including `_pubky.` URLs use crate::PublicKey; -use crate::errors::{PkarrError, Result}; +use crate::errors::{PkarrError, RequestError, Result}; use crate::{PubkyHttpClient, cross_log}; use futures_lite::StreamExt; use pkarr::extra::endpoints::Endpoint; @@ -42,16 +42,36 @@ impl PubkyHttpClient { pub async fn prepare_request(&self, url: &mut Url) -> Result> { let host = url.host_str().unwrap_or("").to_string(); + let invalid_prefixed_host = |value: &str| -> bool { + matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) + }; + let mut pubky_host = None; if let Some(stripped) = host.strip_prefix("_pubky.") { + if invalid_prefixed_host(stripped) { + return Err(RequestError::Validation { + message: "pubky prefix is not allowed in transport hosts; use raw z32" + .to_string(), + } + .into()); + } if PublicKey::try_from(stripped).is_ok() { self.transform_url(url).await?; pubky_host = Some(stripped.to_string()); } - } else if PublicKey::try_from(host.clone()).is_ok() { - self.transform_url(url).await?; - pubky_host = Some(host); + } else { + if invalid_prefixed_host(&host) { + return Err(RequestError::Validation { + message: "pubky prefix is not allowed in transport hosts; use raw z32" + .to_string(), + } + .into()); + } + if PublicKey::try_from(host.clone()).is_ok() { + self.transform_url(url).await?; + pubky_host = Some(host); + } } Ok(pubky_host) From 4ae962c1313eaf131174362ee4ea4a791295a74b Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 2 Jan 2026 14:49:03 +0100 Subject: [PATCH 11/21] ensure no double encoding --- e2e/src/tests/events.rs | 4 +- e2e/src/tests/storage.rs | 122 ++++++++---------- .../src/client_server/layers/trace.rs | 2 +- .../src/client_server/routes/events.rs | 2 +- pubky-sdk/bindings/js/src/pubky.rs | 2 +- pubky-sdk/src/actors/session/core.rs | 2 +- pubky-sdk/src/actors/storage/resource.rs | 2 +- pubky-sdk/src/pubky.rs | 2 +- 8 files changed, 62 insertions(+), 76 deletions(-) diff --git a/e2e/src/tests/events.rs b/e2e/src/tests/events.rs index 7ccc9c82..c4cf9f95 100644 --- a/e2e/src/tests/events.rs +++ b/e2e/src/tests/events.rs @@ -708,9 +708,9 @@ async fn events_stream_multiple_users() { let cursor = cursor_line.strip_prefix("cursor: ").unwrap().to_string(); // Determine which user this event belongs to - if lines[0].contains(&pubky1.to_string()) { + if lines[0].contains(&pubky1.z32()) { user1_cursor = cursor; - } else if lines[0].contains(&pubky2.to_string()) { + } else if lines[0].contains(&pubky2.z32()) { user2_cursor = cursor; } } diff --git a/e2e/src/tests/storage.rs b/e2e/src/tests/storage.rs index ab7392ab..73e580a5 100644 --- a/e2e/src/tests/storage.rs +++ b/e2e/src/tests/storage.rs @@ -33,7 +33,7 @@ async fn put_get_delete() { // Use Pubky native method to get data from homeserver let response = pubky .public_storage() - .get(format!("pubky{public_key}/{path}")) + .get(format!("{public_key}/{}", path.trim_start_matches('/'))) .await .unwrap(); @@ -115,7 +115,7 @@ async fn put_then_get_json_roundtrip() { // Read back as strongly-typed JSON and assert equality. let got: Payload = pubky .public_storage() - .get_json(format!("pubky{}/{path}", public_key)) + .get_json(format!("{}/{}", public_key, path.trim_start_matches('/'))) .await .unwrap(); assert_eq!(got, expected); @@ -229,7 +229,7 @@ async fn unauthorized_put_delete() { // Someone tries to write to owner's namespace -> 401 Unauthorized let owner_url = format!( - "pubky{}/{}", + "{}/{}", owner_session.info().public_key(), path.trim_start_matches('/') ); @@ -321,19 +321,19 @@ async fn list_deep() { assert_eq!( list, vec![ - format!("pubky{public_key}/pub/example.com/a.txt") + format!("{public_key}/pub/example.com/a.txt") .parse() .unwrap(), - format!("pubky{public_key}/pub/example.com/b.txt") + format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("pubky{public_key}/pub/example.com/c.txt") + format!("{public_key}/pub/example.com/c.txt") .parse() .unwrap(), - format!("pubky{public_key}/pub/example.com/cc-nested/z.txt") + format!("{public_key}/pub/example.com/cc-nested/z.txt") .parse() .unwrap(), - format!("pubky{public_key}/pub/example.com/d.txt") + format!("{public_key}/pub/example.com/d.txt") .parse() .unwrap(), ], @@ -354,10 +354,10 @@ async fn list_deep() { assert_eq!( list, vec![ - format!("pubky{public_key}/pub/example.com/a.txt") + format!("{public_key}/pub/example.com/a.txt") .parse() .unwrap(), - format!("pubky{public_key}/pub/example.com/b.txt") + format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), ], @@ -380,10 +380,10 @@ async fn list_deep() { assert_eq!( list, vec![ - format!("pubky{public_key}/pub/example.com/b.txt") + format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("pubky{public_key}/pub/example.com/c.txt") + format!("{public_key}/pub/example.com/c.txt") .parse() .unwrap(), ], @@ -405,10 +405,10 @@ async fn list_deep() { assert_eq!( list, vec![ - format!("pubky{public_key}/pub/example.com/b.txt") + format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("pubky{public_key}/pub/example.com/c.txt") + format!("{public_key}/pub/example.com/c.txt") .parse() .unwrap(), ], @@ -462,19 +462,13 @@ async fn list_shallow() { assert_eq!( list, vec![ - format!("pubky{public_key}/pub/a.com/").parse().unwrap(), - format!("pubky{public_key}/pub/example.com/") - .parse() - .unwrap(), - format!("pubky{public_key}/pub/example.con") - .parse() - .unwrap(), - format!("pubky{public_key}/pub/example.con/") - .parse() - .unwrap(), - format!("pubky{public_key}/pub/file").parse().unwrap(), - format!("pubky{public_key}/pub/file2").parse().unwrap(), - format!("pubky{public_key}/pub/z.com/").parse().unwrap(), + format!("{public_key}/pub/a.com/").parse().unwrap(), + format!("{public_key}/pub/example.com/").parse().unwrap(), + format!("{public_key}/pub/example.con").parse().unwrap(), + format!("{public_key}/pub/example.con/").parse().unwrap(), + format!("{public_key}/pub/file").parse().unwrap(), + format!("{public_key}/pub/file2").parse().unwrap(), + format!("{public_key}/pub/z.com/").parse().unwrap(), ], "normal list shallow" ); @@ -495,10 +489,8 @@ async fn list_shallow() { assert_eq!( list, vec![ - format!("pubky{public_key}/pub/a.com/").parse().unwrap(), - format!("pubky{public_key}/pub/example.com/") - .parse() - .unwrap(), + format!("{public_key}/pub/a.com/").parse().unwrap(), + format!("{public_key}/pub/example.com/").parse().unwrap(), ], "normal list shallow with limit but no cursor" ); @@ -519,12 +511,8 @@ async fn list_shallow() { assert_eq!( list1, vec![ - format!("pubky{public_key}/pub/example.con") - .parse() - .unwrap(), - format!("pubky{public_key}/pub/example.con/") - .parse() - .unwrap(), + format!("{public_key}/pub/example.con").parse().unwrap(), + format!("{public_key}/pub/example.con/").parse().unwrap(), ], "normal list shallow with limit and a file cursor" ); @@ -561,13 +549,9 @@ async fn list_shallow() { assert_eq!( list, vec![ - format!("pubky{public_key}/pub/example.con") - .parse() - .unwrap(), - format!("pubky{public_key}/pub/example.con/") - .parse() - .unwrap(), - format!("pubky{public_key}/pub/file").parse().unwrap(), + format!("{public_key}/pub/example.con").parse().unwrap(), + format!("{public_key}/pub/example.con/").parse().unwrap(), + format!("{public_key}/pub/file").parse().unwrap(), ], "normal list shallow with limit and a directory cursor" ); @@ -584,6 +568,7 @@ async fn list_events() { // Create a user/session let signer = pubky.signer(Keypair::random()); let public_key = signer.public_key(); + let public_key_z32 = public_key.z32(); let session = signer.signup(&server.public_key(), None).await.unwrap(); // Write + delete a bunch of files to populate the event feed @@ -632,16 +617,16 @@ async fn list_events() { assert_eq!( lines, vec![ - format!("PUT pubky://{public_key}/pub/a.com/a.txt"), - format!("DEL pubky://{public_key}/pub/a.com/a.txt"), - format!("PUT pubky://{public_key}/pub/example.com/a.txt"), - format!("DEL pubky://{public_key}/pub/example.com/a.txt"), - format!("PUT pubky://{public_key}/pub/example.com/b.txt"), - format!("DEL pubky://{public_key}/pub/example.com/b.txt"), - format!("PUT pubky://{public_key}/pub/example.com/c.txt"), - format!("DEL pubky://{public_key}/pub/example.com/c.txt"), - format!("PUT pubky://{public_key}/pub/example.com/d.txt"), - format!("DEL pubky://{public_key}/pub/example.com/d.txt"), + format!("PUT pubky://{public_key_z32}/pub/a.com/a.txt"), + format!("DEL pubky://{public_key_z32}/pub/a.com/a.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/a.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/a.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/b.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/b.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/c.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/c.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/d.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/d.txt"), format!("cursor: {cursor}"), ] ); @@ -665,16 +650,16 @@ async fn list_events() { assert_eq!( lines, vec![ - format!("PUT pubky://{public_key}/pub/example.xyz/d.txt"), - format!("DEL pubky://{public_key}/pub/example.xyz/d.txt"), - format!("PUT pubky://{public_key}/pub/example.xyz"), - format!("DEL pubky://{public_key}/pub/example.xyz"), - format!("PUT pubky://{public_key}/pub/file"), - format!("DEL pubky://{public_key}/pub/file"), - format!("PUT pubky://{public_key}/pub/file2"), - format!("DEL pubky://{public_key}/pub/file2"), - format!("PUT pubky://{public_key}/pub/z.com/a.txt"), - format!("DEL pubky://{public_key}/pub/z.com/a.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.xyz/d.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.xyz/d.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.xyz"), + format!("DEL pubky://{public_key_z32}/pub/example.xyz"), + format!("PUT pubky://{public_key_z32}/pub/file"), + format!("DEL pubky://{public_key_z32}/pub/file"), + format!("PUT pubky://{public_key_z32}/pub/file2"), + format!("DEL pubky://{public_key_z32}/pub/file2"), + format!("PUT pubky://{public_key_z32}/pub/z.com/a.txt"), + format!("DEL pubky://{public_key_z32}/pub/z.com/a.txt"), lines.last().unwrap().to_string(), ] ); @@ -691,10 +676,11 @@ async fn read_after_event() { // User + session let signer = pubky.signer(Keypair::random()); let public_key = signer.public_key(); + let public_key_z32 = public_key.z32(); let session = signer.signup(&server.public_key(), None).await.unwrap(); // Write one file - let url = format!("pubky://{public_key}/pub/a.com/a.txt"); + let url = format!("pubky://{public_key_z32}/pub/a.com/a.txt"); session .storage() .put("/pub/a.com/a.txt", vec![0]) @@ -789,9 +775,9 @@ async fn dont_delete_shared_blobs() { 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"), + format!("PUT pubky://{}/pub/pubky.app/file/file_1", user_1_id.z32()), + format!("PUT pubky://{}/pub/pubky.app/file/file_1", user_2_id.z32()), + format!("DEL pubky://{}/pub/pubky.app/file/file_1", user_1_id.z32()), lines.last().unwrap().to_string(), ] ); diff --git a/pubky-homeserver/src/client_server/layers/trace.rs b/pubky-homeserver/src/client_server/layers/trace.rs index c3f0a224..02c36e0c 100644 --- a/pubky-homeserver/src/client_server/layers/trace.rs +++ b/pubky-homeserver/src/client_server/layers/trace.rs @@ -24,7 +24,7 @@ pub fn with_trace_layer(router: Router) -> Router { TraceLayer::new_for_http() .make_span_with(move |request: &Request| { let uri = if let Some(pubky_host) = request.extensions().get::() { - format!("pubky://{pubky_host}{}", request.uri()) + format!("pubky://{}{}", pubky_host.public_key().z32(), request.uri()) } else { request.uri().to_string() }; diff --git a/pubky-homeserver/src/client_server/routes/events.rs b/pubky-homeserver/src/client_server/routes/events.rs index cb57839f..3f158e69 100644 --- a/pubky-homeserver/src/client_server/routes/events.rs +++ b/pubky-homeserver/src/client_server/routes/events.rs @@ -209,7 +209,7 @@ fn formatted_event_path(entity: &EventEntity) -> String { // TODO: switch this formatter to use the shared `PubkyResource` type from `pubky-sdk` // once the homeserver crate depends on it directly, so we avoid ad-hoc string // reconstruction here. - format!("pubky://{}{}", entity.user_pubkey, entity.path.path()) + format!("pubky://{}{}", entity.user_pubkey.z32(), entity.path.path()) } fn event_to_sse_data(entity: &EventEntity) -> String { diff --git a/pubky-sdk/bindings/js/src/pubky.rs b/pubky-sdk/bindings/js/src/pubky.rs index 5e62f86f..1831b597 100644 --- a/pubky-sdk/bindings/js/src/pubky.rs +++ b/pubky-sdk/bindings/js/src/pubky.rs @@ -145,7 +145,7 @@ impl Pubky { /// Use this for low-level `fetch()` calls or testing with raw URLs. /// /// @example - /// const r = await pubky.client.fetch(`pubky://${user}/pub/app/file.txt`, { credentials: "include" }); + /// const r = await pubky.client.fetch(`pubky://${userPk.z32()}/pub/app/file.txt`, { credentials: "include" }); #[wasm_bindgen(getter)] pub fn client(&self) -> Client { Client(self.0.client().clone()) diff --git a/pubky-sdk/src/actors/session/core.rs b/pubky-sdk/src/actors/session/core.rs index 6e3281a2..a47b1009 100644 --- a/pubky-sdk/src/actors/session/core.rs +++ b/pubky-sdk/src/actors/session/core.rs @@ -47,7 +47,7 @@ impl PubkySession { /// This POSTs the resolved homeserver session endpoint with the token, validates the response /// and constructs a new session-bound [`PubkySession`] pub(crate) async fn new(token: &AuthToken, client: PubkyHttpClient) -> Result { - let url = format!("pubky://{}/session", token.public_key()); + let url = format!("pubky://{}/session", token.public_key().z32()); cross_log!( info, "Establishing new session exchange for {}", diff --git a/pubky-sdk/src/actors/storage/resource.rs b/pubky-sdk/src/actors/storage/resource.rs index 70706491..d66c21df 100644 --- a/pubky-sdk/src/actors/storage/resource.rs +++ b/pubky-sdk/src/actors/storage/resource.rs @@ -393,7 +393,7 @@ impl IntoResourcePath for &String { /// let r2: PubkyResource = format!("{}/pub/site/index.html", user).parse()?; /// /// // Parse `pubky://` -/// let r3: PubkyResource = format!("pubky://{}/pub/site/index.html", user).parse()?; +/// let r3: PubkyResource = format!("pubky://{}/pub/site/index.html", user.z32()).parse()?; /// # Ok::<(), pubky::Error>(()) /// ``` pub trait IntoPubkyResource { diff --git a/pubky-sdk/src/pubky.rs b/pubky-sdk/src/pubky.rs index dd507ff7..9cf32f38 100644 --- a/pubky-sdk/src/pubky.rs +++ b/pubky-sdk/src/pubky.rs @@ -45,7 +45,7 @@ //! # async fn run(user: pubky::PublicKey) -> pubky::Result<()> { //! let pubky = Pubky::new()?; //! let public = pubky.public_storage(); -//! let addr = format!("pubky{}/pub/site/index.html", user); +//! let addr = format!("{}/pub/site/index.html", user); //! let html = public.get(addr).await?.text().await?; //! # Ok(()) } //! ``` From 668ee4d584f08667f557d98d141622b3f674197f Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 2 Jan 2026 16:43:46 +0100 Subject: [PATCH 12/21] fix docs and enfore non-prefixed keys on transport --- .../src/client_server/routes/tenants/read.rs | 28 ++++++++-------- pubky-sdk/bindings/js/src/wrappers/keys.rs | 4 +-- pubky-sdk/src/client/http_targets/native.rs | 32 ++++++++++++++++--- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/pubky-homeserver/src/client_server/routes/tenants/read.rs b/pubky-homeserver/src/client_server/routes/tenants/read.rs index fab4e043..63b521d7 100644 --- a/pubky-homeserver/src/client_server/routes/tenants/read.rs +++ b/pubky-homeserver/src/client_server/routes/tenants/read.rs @@ -294,7 +294,7 @@ mod tests { server .put("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header(header::COOKIE, cookie) .bytes(data.into()) .expect_success() @@ -302,13 +302,13 @@ mod tests { let response = server .get("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .expect_success() .await; let response = server .get("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header( header::IF_MODIFIED_SINCE, response.headers().get(header::LAST_MODIFIED).unwrap(), @@ -327,7 +327,7 @@ mod tests { server .put("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header(header::COOKIE, cookie) .bytes(data.into()) .expect_success() @@ -335,13 +335,13 @@ mod tests { let response = server .get("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .expect_success() .await; let response = server .get("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header( header::IF_NONE_MATCH, response.headers().get(header::ETAG).unwrap(), @@ -360,7 +360,7 @@ mod tests { server .put("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header(header::COOKIE, cookie) .bytes(data.into()) .expect_success() @@ -368,7 +368,7 @@ mod tests { let response = server .get("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .await; response.assert_header(header::CONTENT_TYPE, "image/png"); @@ -383,7 +383,7 @@ mod tests { server .put("/pub/text.txt") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header(header::COOKIE, cookie) .bytes(data.into()) .expect_success() @@ -391,7 +391,7 @@ mod tests { let response = server .get("/pub/text.txt") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .await; response.assert_header(header::CONTENT_TYPE, "text/plain"); @@ -403,7 +403,7 @@ mod tests { // Write v1 server .put("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header(header::COOKIE, cookie.clone()) .bytes(Vec::from("alice").into()) .expect_success() @@ -412,7 +412,7 @@ mod tests { // Baseline GET to capture ETag and Last-Modified let base = server .get("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .expect_success() .await; let etag_v1 = base @@ -427,7 +427,7 @@ mod tests { // Overwrite with different content but same-second timestamp likely server .put("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header(header::COOKIE, cookie.clone()) .bytes(Vec::from("bob").into()) .expect_success() @@ -436,7 +436,7 @@ mod tests { // Conditional GET that sends both validators; must return 200 because ETag changed. let r = server .get("/pub/foo") - .add_header("host", public_key.to_string()) + .add_header("host", public_key.z32()) .add_header(header::IF_NONE_MATCH, etag_v1) .add_header(header::IF_MODIFIED_SINCE, lm_v1) .await; diff --git a/pubky-sdk/bindings/js/src/wrappers/keys.rs b/pubky-sdk/bindings/js/src/wrappers/keys.rs index 233b2944..2149d12a 100644 --- a/pubky-sdk/bindings/js/src/wrappers/keys.rs +++ b/pubky-sdk/bindings/js/src/wrappers/keys.rs @@ -33,11 +33,11 @@ impl Keypair { /// Returns the [PublicKey] of this keypair. /// - /// Use `.to_string()` on the returned `PublicKey` to get the string form. + /// Use `.toString()` on the returned `PublicKey` to get the string form /// or `.z32()` to get the z32 string form without prefix. /// /// @example - /// const who = keypair.publicKey.to_string(); + /// const who = keypair.publicKey.toString(); #[wasm_bindgen(js_name = "publicKey", getter)] pub fn public_key(&self) -> PublicKey { PublicKey(self.0.public_key()) diff --git a/pubky-sdk/src/client/http_targets/native.rs b/pubky-sdk/src/client/http_targets/native.rs index 3600ec3d..b3ac0b42 100644 --- a/pubky-sdk/src/client/http_targets/native.rs +++ b/pubky-sdk/src/client/http_targets/native.rs @@ -1,3 +1,4 @@ +use crate::errors::RequestError; use crate::{PubkyHttpClient, PublicKey, Result, cross_log}; use reqwest::{IntoUrl, Method, RequestBuilder}; use url::Url; @@ -11,15 +12,22 @@ enum HostKind { fn classify_host(host: &str) -> HostKind { if let Some(pk_host) = host.strip_prefix("_pubky.") { + if is_prefixed_pubky_host(pk_host) { + return HostKind::Icann; + } if PublicKey::try_from(pk_host).is_ok() { return HostKind::ResolvedPubky; } - } else if PublicKey::try_from(host).is_err() { + } else if is_prefixed_pubky_host(host) || PublicKey::try_from(host).is_err() { return HostKind::Icann; } HostKind::Pubky } +fn is_prefixed_pubky_host(value: &str) -> bool { + matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) +} + impl PubkyHttpClient { /// Constructs a [`reqwest::RequestBuilder`] for the given HTTP `method` and `url`, /// routing through the client’s unified request path. @@ -50,7 +58,7 @@ impl PubkyHttpClient { /// `pubky-host` value when applicable. /// /// # Errors - /// - This function does not currently return errors; it keeps the `Result` to match WASM. + /// - Returns [`crate::errors::RequestError::Validation`] if the host uses a `pubky` prefix. #[allow( clippy::unused_async, reason = "keep async signature aligned with WASM build" @@ -59,11 +67,27 @@ impl PubkyHttpClient { let host = url.host_str().unwrap_or(""); if let Some(stripped) = host.strip_prefix("_pubky.") { + if is_prefixed_pubky_host(stripped) { + return Err(RequestError::Validation { + message: "pubky prefix is not allowed in transport hosts; use raw z32" + .to_string(), + } + .into()); + } if PublicKey::try_from(stripped).is_ok() { return Ok(Some(stripped.to_string())); } - } else if PublicKey::try_from(host).is_ok() { - return Ok(Some(host.to_string())); + } else { + if is_prefixed_pubky_host(host) { + return Err(RequestError::Validation { + message: "pubky prefix is not allowed in transport hosts; use raw z32" + .to_string(), + } + .into()); + } + if PublicKey::try_from(host).is_ok() { + return Ok(Some(host.to_string())); + } } Ok(None) From f09c91084d25fab457ed7c3fae2ac1fa962277d6 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Sat, 3 Jan 2026 05:26:16 +0100 Subject: [PATCH 13/21] fix docs, remove collate C --- .../src/client_server/routes/tenants/read.rs | 10 ++-- .../sql/entities/entry/repository.rs | 52 ++++++++----------- pubky-sdk/README.md | 16 +++--- pubky-sdk/bindings/js/pkg/README.md | 27 +++++----- .../bindings/js/src/actors/storage/public.rs | 2 +- pubky-sdk/bindings/js/src/pubky.rs | 6 +-- .../bindings/js/src/wrappers/auth_token.rs | 11 ++-- .../bindings/js/src/wrappers/session_info.rs | 5 +- pubky-sdk/src/actors/storage/resource.rs | 12 +++++ 9 files changed, 75 insertions(+), 66 deletions(-) diff --git a/pubky-homeserver/src/client_server/routes/tenants/read.rs b/pubky-homeserver/src/client_server/routes/tenants/read.rs index 63b521d7..d2748699 100644 --- a/pubky-homeserver/src/client_server/routes/tenants/read.rs +++ b/pubky-homeserver/src/client_server/routes/tenants/read.rs @@ -235,9 +235,10 @@ mod tests { use axum::http::{header, StatusCode}; use axum::Router; use axum_test::TestServer; - use pkarr::{Keypair, PublicKey}; use pubky_common::{ - auth::AuthToken, capabilities::Capability, crypto::Keypair as PubkyKeypair, + auth::AuthToken, + capabilities::Capability, + crypto::{Keypair, PublicKey}, }; use crate::app_context::AppContext; @@ -247,10 +248,7 @@ mod tests { server: &axum_test::TestServer, keypair: &Keypair, ) -> anyhow::Result { - let auth_token = AuthToken::sign( - &PubkyKeypair::from(keypair.clone()), - vec![Capability::root()], - ); + let auth_token = AuthToken::sign(keypair, vec![Capability::root()]); let body_bytes: axum::body::Bytes = auth_token.serialize().into(); let response = server .post("/signup") diff --git a/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs b/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs index 55ef72a6..f18dba3c 100644 --- a/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs +++ b/pubky-homeserver/src/persistence/sql/entities/entry/repository.rs @@ -266,26 +266,22 @@ impl EntryRepository { .from_subquery(inner_statement, Alias::new("t")) .to_owned(); - let regpath_order_expr = Expr::cust("regpath COLLATE \"C\""); if reverse { - outer_statement = outer_statement - .order_by_expr(regpath_order_expr.clone(), Order::Desc) - .to_owned(); + outer_statement = outer_statement.order_by("regpath", Order::Desc).to_owned(); } else { - outer_statement = outer_statement - .order_by_expr(regpath_order_expr.clone(), Order::Asc) - .to_owned(); + outer_statement = outer_statement.order_by("regpath", Order::Asc).to_owned(); } if let Some(cursor_entry_path) = cursor { - let op = if reverse { "<" } else { ">" }; - let cursor_expr = Expr::cust_with_values( - format!("regpath COLLATE \"C\" {op} $1"), - vec![sea_query::Value::from( - cursor_entry_path.path().as_str().to_string(), - )], - ); - outer_statement = outer_statement.and_where(cursor_expr).to_owned(); + if reverse { + outer_statement = outer_statement + .and_where(Expr::col("regpath").lt(cursor_entry_path.path().as_str())) + .to_owned(); + } else { + outer_statement = outer_statement + .and_where(Expr::col("regpath").gt(cursor_entry_path.path().as_str())) + .to_owned(); + } } let limit = limit.unwrap_or(DEFAULT_LIST_LIMIT); @@ -340,30 +336,26 @@ impl EntryRepository { .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32())) .to_owned(); - let path_order_expr = Expr::cust(format!( - "{ENTRY_TABLE}.{} COLLATE \"C\"", - EntryIden::Path.to_string() - )); if reverse { statement = statement - .order_by_expr(path_order_expr.clone(), Order::Desc) + .order_by((ENTRY_TABLE, EntryIden::Path), Order::Desc) .to_owned(); } else { statement = statement - .order_by_expr(path_order_expr.clone(), Order::Asc) + .order_by((ENTRY_TABLE, EntryIden::Path), Order::Asc) .to_owned(); } if let Some(cursor) = cursor { - let op = if reverse { "<" } else { ">" }; - let cursor_expr = Expr::cust_with_values( - format!( - "{ENTRY_TABLE}.{} COLLATE \"C\" {op} $1", - EntryIden::Path.to_string() - ), - vec![sea_query::Value::from(cursor.path().as_str().to_string())], - ); - statement = statement.and_where(cursor_expr).to_owned(); + if reverse { + statement = statement + .and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).lt(cursor.path().as_str())) + .to_owned(); + } else { + statement = statement + .and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).gt(cursor.path().as_str())) + .to_owned(); + } } let limit = limit.unwrap_or(DEFAULT_LIST_LIMIT); diff --git a/pubky-sdk/README.md b/pubky-sdk/README.md index 6ba09b88..a58a73d7 100644 --- a/pubky-sdk/README.md +++ b/pubky-sdk/README.md @@ -37,10 +37,14 @@ let body = session.storage().get("/pub/my-cool-app/hello.txt").await?.text().awa assert_eq!(&body, "hello"); // 4) Public read of another user’s file -let txt = pubky.public_storage() - .get(format!("pubky{}/pub/my-cool-app/hello.txt", session.info().public_key())) - .await? - .text().await?; +let txt = pubky + .public_storage() + .get(format!( + "{}/pub/my-cool-app/hello.txt", + session.info().public_key() + )) + .await? + .text().await?; assert_eq!(txt, "hello"); // 5) Keyless app flow (QR/deeplink) @@ -101,13 +105,13 @@ let pubky = Pubky::new()?; let public = pubky.public_storage(); let file = public - .get(format!("pubky{user_id}/pub/example.com/file.bin")) + .get(format!("{user_id}/pub/example.com/file.bin")) .await? .bytes() .await?; let entries = public - .list(format!("pubky{user_id}/pub/example.com/"))? + .list(format!("{user_id}/pub/example.com/"))? .limit(10) .send() .await?; diff --git a/pubky-sdk/bindings/js/pkg/README.md b/pubky-sdk/bindings/js/pkg/README.md index 61ae9f8b..586c219e 100644 --- a/pubky-sdk/bindings/js/pkg/README.md +++ b/pubky-sdk/bindings/js/pkg/README.md @@ -38,8 +38,8 @@ const path = "/pub/example.com/hello.json"; await session.storage.putJson(path, { hello: "world" }); // 4) Read it publicly (no auth needed) -const userPk = session.info.publicKey.z32(); -const addr = `pubky${userPk}/pub/example.com/hello.json`; +const userPk = session.info.publicKey.toString(); +const addr = `${userPk}/pub/example.com/hello.json`; const json = await pubky.publicStorage.getJson(addr); // -> { hello: "world" } // 5) Authenticate on a 3rd-party app @@ -87,12 +87,13 @@ const client = pubky.client; ### Client (HTTP bridge) ```js -import { Client, resolvePubky } from "@synonymdev/pubky"; +import { Client, PublicKey, resolvePubky } from "@synonymdev/pubky"; const client = new Client(); // or: pubky.client.fetch(); instead of constructing a client manually // Convert the identifier into a transport URL before fetching. -const url = resolvePubky("pubky/pub/example.com/file.txt"); +const userId = PublicKey.from("pubky").toString(); +const url = resolvePubky(`${userId}/pub/example.com/file.txt`); const res = await client.fetch(url); ``` @@ -151,7 +152,7 @@ await session.signout(); // invalidates server session **Session details** ```js -const userPk = session.info.publicKey.z32(); // -> PublicKey as z32 string +const userPk = session.info.publicKey.toString(); // -> pubky identifier const caps = session.info.capabilities; // -> string[] permissions and paths const storage = session.storage; // -> This User's storage API (absolute paths) @@ -252,20 +253,20 @@ const pub = pubky.publicStorage; // Reads const response = await pub.get( - `pubky${userPk.z32()}/pub/example.com/data.json` + `${userPk}/pub/example.com/data.json` ); // -> Response (stream it) -await pub.getJson(`pubky${userPk.z32()}/pub/example.com/data.json`); -await pub.getText(`pubky${userPk.z32()}/pub/example.com/readme.txt`); -await pub.getBytes(`pubky${userPk.z32()}/pub/example.com/icon.png`); // Uint8Array +await pub.getJson(`${userPk}/pub/example.com/data.json`); +await pub.getText(`${userPk}/pub/example.com/readme.txt`); +await pub.getBytes(`${userPk}/pub/example.com/icon.png`); // Uint8Array // Metadata -await pub.exists(`pubky${userPk.z32()}/pub/example.com/foo`); // boolean -await pub.stats(`pubky${userPk.z32()}/pub/example.com/foo`); // { content_length, content_type, etag, last_modified } | null +await pub.exists(`${userPk}/pub/example.com/foo`); // boolean +await pub.stats(`${userPk}/pub/example.com/foo`); // { content_length, content_type, etag, last_modified } | null // List directory (addressed path "/pub/.../") must include trailing `/`. // list(addr, cursor=null|suffix|fullUrl, reverse=false, limit?, shallow=false) await pub.list( - `pubky${userPk.z32()}/pub/example.com/`, + `${userPk}/pub/example.com/`, null, false, 100, @@ -323,7 +324,7 @@ import { Pubky, PublicKey, Keypair } from "@synonymdev/pubky"; const pubky = new Pubky(); // Read-only resolver -const homeserver = await pubky.getHomeserverOf(PublicKey.from("")); // string | undefined +const homeserver = await pubky.getHomeserverOf(PublicKey.from("pubky")); // string | undefined // With keys (signer-bound) const signer = pubky.signer(Keypair.random()); diff --git a/pubky-sdk/bindings/js/src/actors/storage/public.rs b/pubky-sdk/bindings/js/src/actors/storage/public.rs index fcf65741..67172fe9 100644 --- a/pubky-sdk/bindings/js/src/actors/storage/public.rs +++ b/pubky-sdk/bindings/js/src/actors/storage/public.rs @@ -11,7 +11,7 @@ use crate::js_error::JsResult; const TS_ADDRESS: &'static str = r#"export type Address = `pubky${string}/pub/${string}` | `pubky://${string}/pub/${string}`;"#; -/// Read-only public storage using addressed paths (`"/pub/...")`. +/// Read-only public storage using addressed paths (`"pubky/pub/..."`). #[wasm_bindgen] pub struct PublicStorage(pub(crate) pubky::PublicStorage); diff --git a/pubky-sdk/bindings/js/src/pubky.rs b/pubky-sdk/bindings/js/src/pubky.rs index 1831b597..ff139367 100644 --- a/pubky-sdk/bindings/js/src/pubky.rs +++ b/pubky-sdk/bindings/js/src/pubky.rs @@ -114,12 +114,12 @@ impl Pubky { /// Public, unauthenticated storage API. /// /// Use for **read-only** public access via addressed paths: - /// `"/pub/…"`. + /// `"pubky/pub/…"`. /// /// @returns {PublicStorage} /// /// @example - /// const text = await pubky.publicStorage.getText(`${userPk.z32()}/pub/example.com/hello.txt`); + /// const text = await pubky.publicStorage.getText(`${userPk.toString()}/pub/example.com/hello.txt`); #[wasm_bindgen(js_name = "publicStorage", getter)] pub fn public_storage(&self) -> PublicStorage { PublicStorage(self.0.public_storage()) @@ -130,7 +130,7 @@ impl Pubky { /// Uses an internal read-only Pkdns actor. /// /// @param {PublicKey} user - /// @returns {Promise} Homeserver public key (z32) or `undefined` if not found. + /// @returns {Promise} Homeserver public key or `undefined` if not found. #[wasm_bindgen(js_name = "getHomeserverOf")] pub async fn get_homeserver_of(&self, user_public_key: &PublicKey) -> Option { self.0 diff --git a/pubky-sdk/bindings/js/src/wrappers/auth_token.rs b/pubky-sdk/bindings/js/src/wrappers/auth_token.rs index 2daed1a6..4d832185 100644 --- a/pubky-sdk/bindings/js/src/wrappers/auth_token.rs +++ b/pubky-sdk/bindings/js/src/wrappers/auth_token.rs @@ -12,7 +12,7 @@ // const token = await flow.awaitToken(); // <- AuthToken // // // Who just authenticated? -// console.log(token.publicKey().z32()); +// console.log(token.publicKey().toString()); // // // Optional: send to your backend and verify there // const bytes = token.toBytes(); // Uint8Array @@ -42,7 +42,7 @@ use crate::wrappers::keys::PublicKey; /// const token = await flow.awaitToken(); /// /// // Identify the user -/// console.log(token.publicKey().z32()); +/// console.log(token.publicKey().toString()); /// /// // Optionally forward to a server for verification: /// await fetch("/api/verify", { method: "POST", body: token.toBytes() }); @@ -73,7 +73,7 @@ impl AuthToken { /// export async function POST(req) { /// const bytes = new Uint8Array(await req.arrayBuffer()); /// const token = AuthToken.verify(bytes); // throws on failure - /// return new Response(token.publicKey().z32(), { status: 200 }); + /// return new Response(token.publicKey().toString(), { status: 200 }); /// } /// ``` #[wasm_bindgen(js_name = "verify")] @@ -102,10 +102,11 @@ impl AuthToken { /// Returns the **public key** that authenticated with this token. /// - /// Use `.z32()` on the returned `PublicKey` to get the string form. + /// Use `.toString()` on the returned `PublicKey` to get the `pubky` identifier. + /// Call `.z32()` when you specifically need the raw z-base32 value (e.g. hostnames). /// /// @example - /// const who = sessionInfo.publicKey.z32(); + /// const who = token.publicKey.toString(); #[wasm_bindgen(js_name = "publicKey", getter)] pub fn public_key(&self) -> PublicKey { // `pubky::PublicKey` implements `Clone` diff --git a/pubky-sdk/bindings/js/src/wrappers/session_info.rs b/pubky-sdk/bindings/js/src/wrappers/session_info.rs index 4ae9a942..aceda5bb 100644 --- a/pubky-sdk/bindings/js/src/wrappers/session_info.rs +++ b/pubky-sdk/bindings/js/src/wrappers/session_info.rs @@ -12,12 +12,13 @@ pub struct SessionInfo(pub(crate) session::SessionInfo); impl SessionInfo { /// The user’s public key for this session. /// - /// Use `.z32()` on the returned `PublicKey` to get the string form. + /// Use `.toString()` on the returned `PublicKey` to get the `pubky` identifier. + /// Call `.z32()` when you specifically need the raw z-base32 value (e.g. hostnames). /// /// @returns {PublicKey} /// /// @example - /// const who = sessionInfo.publicKey.z32(); + /// const who = sessionInfo.publicKey.toString(); #[wasm_bindgen(js_name = "publicKey", getter)] pub fn public_key(&self) -> PublicKey { self.0.public_key().clone().into() diff --git a/pubky-sdk/src/actors/storage/resource.rs b/pubky-sdk/src/actors/storage/resource.rs index d66c21df..31d9cde9 100644 --- a/pubky-sdk/src/actors/storage/resource.rs +++ b/pubky-sdk/src/actors/storage/resource.rs @@ -261,11 +261,18 @@ impl FromStr for PubkyResource { type Err = Error; fn from_str(s: &str) -> Result { + let is_prefixed_pubky = |value: &str| matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52); + // 1) pubky:/// if let Some(rest) = s.strip_prefix("pubky://") { let (user_str, path) = rest .split_once('/') .ok_or_else(|| invalid("missing `/`"))?; + if is_prefixed_pubky(user_str) { + return Err(invalid( + "unexpected `pubky` prefix in user id; use raw z32 after `pubky://`", + )); + } let user = PublicKey::try_from(user_str) .map_err(|_err| invalid(format!("invalid user public key: {user_str}")))?; return Self::new(user, path); @@ -274,6 +281,11 @@ impl FromStr for PubkyResource { // 2) pubky/ if let Some(rest) = s.strip_prefix("pubky") { if let Some((user_id, path)) = rest.split_once('/') { + if is_prefixed_pubky(user_id) { + return Err(invalid( + "unexpected `pubky` prefix in user id; use raw z32 after `pubky`", + )); + } let user = PublicKey::try_from(user_id).map_err(|_err| { invalid("expected `pubky/` or `pubky:///`") })?; From bc12a8486839b8e2d9e0ec5d7ac1ebe3b5e8839b Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Sat, 3 Jan 2026 12:53:25 +0100 Subject: [PATCH 14/21] more strict where z32 only is expected with useful messages --- .../src/admin_server/routes/disable_users.rs | 5 +++-- .../src/client_server/layers/pubky_host.rs | 10 ++++++++++ pubky-homeserver/src/shared/pubkey_path_validator.rs | 9 +++++++++ pubky-sdk/src/actors/storage/resource.rs | 12 ++++++++++-- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/pubky-homeserver/src/admin_server/routes/disable_users.rs b/pubky-homeserver/src/admin_server/routes/disable_users.rs index bae8e96e..8de4c509 100644 --- a/pubky-homeserver/src/admin_server/routes/disable_users.rs +++ b/pubky-homeserver/src/admin_server/routes/disable_users.rs @@ -106,8 +106,9 @@ mod tests { // Disable the tenant let server = axum_test::TestServer::new(router).unwrap(); + let pubkey_path = pubkey.z32(); let response = server - .post(format!("/users/{}/disable", pubkey).as_str()) + .post(format!("/users/{}/disable", pubkey_path).as_str()) .await; assert_eq!(response.status_code(), StatusCode::OK); @@ -119,7 +120,7 @@ mod tests { // Enable the tenant again let response = server - .post(format!("/users/{}/enable", pubkey).as_str()) + .post(format!("/users/{}/enable", pubkey_path).as_str()) .await; assert_eq!(response.status_code(), StatusCode::OK); diff --git a/pubky-homeserver/src/client_server/layers/pubky_host.rs b/pubky-homeserver/src/client_server/layers/pubky_host.rs index d6d1a237..a7729b3f 100644 --- a/pubky-homeserver/src/client_server/layers/pubky_host.rs +++ b/pubky-homeserver/src/client_server/layers/pubky_host.rs @@ -58,6 +58,9 @@ fn extract_pubky(req: &Request) -> Option { for header in ["host", "pubky-host"].iter() { if let Some(val) = req.headers().get(*header) { if let Ok(s) = val.to_str() { + if is_prefixed_pubky(s) { + continue; + } if let Ok(key) = PublicKey::try_from(s) { pubky = Some(key); } @@ -71,6 +74,9 @@ fn extract_pubky(req: &Request) -> Option { let mut parts = pair.splitn(2, '='); if let (Some(key), Some(val)) = (parts.next(), parts.next()) { if key == "pubky-host" { + if is_prefixed_pubky(val) { + return None; + } return PublicKey::try_from(val).ok(); } } @@ -80,3 +86,7 @@ fn extract_pubky(req: &Request) -> Option { } pubky } + +fn is_prefixed_pubky(value: &str) -> bool { + matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) +} diff --git a/pubky-homeserver/src/shared/pubkey_path_validator.rs b/pubky-homeserver/src/shared/pubkey_path_validator.rs index 34c04085..07d43c8a 100644 --- a/pubky-homeserver/src/shared/pubkey_path_validator.rs +++ b/pubky-homeserver/src/shared/pubkey_path_validator.rs @@ -27,7 +27,16 @@ impl<'de> serde::Deserialize<'de> for Z32Pubkey { D: serde::Deserializer<'de>, { let s: String = serde::Deserialize::deserialize(deserializer)?; + if is_prefixed_pubky(&s) { + return Err(serde::de::Error::custom( + "unexpected `pubky` prefix; expected raw z32", + )); + } let pubkey = PublicKey::try_from(s.as_str()).map_err(serde::de::Error::custom)?; Ok(Z32Pubkey(pubkey)) } } + +fn is_prefixed_pubky(value: &str) -> bool { + matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) +} diff --git a/pubky-sdk/src/actors/storage/resource.rs b/pubky-sdk/src/actors/storage/resource.rs index 31d9cde9..b1298b2e 100644 --- a/pubky-sdk/src/actors/storage/resource.rs +++ b/pubky-sdk/src/actors/storage/resource.rs @@ -33,6 +33,11 @@ fn invalid(msg: impl Into) -> Error { .into() } +#[inline] +fn is_prefixed_pubky(value: &str) -> bool { + matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) +} + // ============================================================================ // ResourcePath // ============================================================================ @@ -239,6 +244,11 @@ impl PubkyResource { let owner = host .strip_prefix("_pubky.") .ok_or_else(|| invalid("transport URL host must start with '_pubky.'"))?; + if is_prefixed_pubky(owner) { + return Err(invalid( + "transport URL host must use raw z32 without `pubky` prefix", + )); + } let public_key = PublicKey::try_from(owner) .map_err(|_err| invalid("transport URL host does not contain a valid public key"))?; @@ -261,8 +271,6 @@ impl FromStr for PubkyResource { type Err = Error; fn from_str(s: &str) -> Result { - let is_prefixed_pubky = |value: &str| matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52); - // 1) pubky:/// if let Some(rest) = s.strip_prefix("pubky://") { let (user_str, path) = rest From dbdc9885f79c8c39c2ff132b891ee44a05407268 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Sat, 3 Jan 2026 14:17:57 +0100 Subject: [PATCH 15/21] centralized is_prefixed_pubky check on pubky-common --- pubky-common/src/crypto.rs | 2 +- pubky-common/src/crypto/keys.rs | 17 +++++++++-- .../src/admin_server/routes/delete_entry.rs | 4 +-- .../src/client_server/layers/pubky_host.rs | 10 ++----- .../src/client_server/routes/auth.rs | 4 +-- .../src/client_server/routes/events.rs | 7 +++-- .../persistence/files/events/events_entity.rs | 6 ++-- .../src/persistence/files/user_quota_layer.rs | 28 +++++++++++++------ .../persistence/sql/entities/signup_code.rs | 4 ++- .../src/persistence/sql/entities/user.rs | 2 +- .../sql/migrations/m20250806_create_user.rs | 2 +- .../m20250812_create_signup_code.rs | 3 +- .../src/shared/pubkey_path_validator.rs | 8 ++---- .../src/shared/webdav/entry_path.rs | 9 ++++-- pubky-sdk/bindings/js/src/wrappers/keys.rs | 17 +++++++---- .../src/actors/auth/deep_links/signup.rs | 2 +- pubky-sdk/src/actors/pkdns.rs | 2 +- pubky-sdk/src/actors/session/persist.rs | 7 +++-- pubky-sdk/src/actors/storage/resource.rs | 12 +++----- pubky-sdk/src/client/http_targets/native.rs | 26 +++++++++-------- pubky-sdk/src/client/http_targets/wasm.rs | 13 ++++----- 21 files changed, 106 insertions(+), 79 deletions(-) diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index a6d0d4fd..439fc214 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -7,7 +7,7 @@ use crypto_secretbox::{ use rand::random; mod keys; -pub use keys::{Keypair, PublicKey}; +pub use keys::{is_prefixed_pubky, Keypair, PublicKey}; pub use ed25519_dalek::Signature; diff --git a/pubky-common/src/crypto/keys.rs b/pubky-common/src/crypto/keys.rs index a9a04ba9..c9a8a824 100644 --- a/pubky-common/src/crypto/keys.rs +++ b/pubky-common/src/crypto/keys.rs @@ -8,10 +8,16 @@ use serde::{Deserialize, Serialize}; type ParseError = >::Error; +/// Returns true if the value is in `pubky` form. +pub fn is_prefixed_pubky(value: &str) -> bool { + matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) +} + fn parse_public_key(value: &str) -> Result { - let raw = match value.strip_prefix("pubky") { - Some(stripped) if stripped.len() == 52 => stripped, - _ => value, + let raw = if is_prefixed_pubky(value) { + value.strip_prefix("pubky").unwrap_or(value) + } else { + value }; pkarr::PublicKey::try_from(raw.to_string()) } @@ -130,6 +136,11 @@ impl PublicKey { pub fn z32(&self) -> String { self.0.to_string() } + + /// Parse a public key from raw z-base32 text (without the `pubky` prefix). + pub fn try_from_z32(value: &str) -> Result { + pkarr::PublicKey::try_from(value.to_string()).map(Self) + } } impl fmt::Display for PublicKey { diff --git a/pubky-homeserver/src/admin_server/routes/delete_entry.rs b/pubky-homeserver/src/admin_server/routes/delete_entry.rs index 030408b9..48fdd003 100644 --- a/pubky-homeserver/src/admin_server/routes/delete_entry.rs +++ b/pubky-homeserver/src/admin_server/routes/delete_entry.rs @@ -63,7 +63,7 @@ mod tests { // Delete the file let server = axum_test::TestServer::new(router).unwrap(); let response = server - .delete(format!("/webdav/{}{}", pubkey, entry_path.path().as_str()).as_str()) + .delete(format!("/webdav/{}", entry_path.as_str()).as_str()) .await; assert_eq!(response.status_code(), StatusCode::NO_CONTENT); @@ -102,7 +102,7 @@ mod tests { .with_state(app_state); // Delete the file - let url = format!("/webdav/{}/pub/{}", pubkey, file_path); + let url = format!("/webdav/{}/pub/{}", pubkey.z32(), file_path); let server = axum_test::TestServer::new(router).unwrap(); let response = server.delete(url.as_str()).await; assert_eq!(response.status_code(), StatusCode::NOT_FOUND); diff --git a/pubky-homeserver/src/client_server/layers/pubky_host.rs b/pubky-homeserver/src/client_server/layers/pubky_host.rs index a7729b3f..a5d454e3 100644 --- a/pubky-homeserver/src/client_server/layers/pubky_host.rs +++ b/pubky-homeserver/src/client_server/layers/pubky_host.rs @@ -1,7 +1,7 @@ use crate::client_server::extractors::PubkyHost; use axum::{body::Body, http::Request}; use futures_util::future::BoxFuture; -use pubky_common::crypto::PublicKey; +use pubky_common::crypto::{is_prefixed_pubky, PublicKey}; use std::{convert::Infallible, task::Poll}; use tower::{Layer, Service}; @@ -61,7 +61,7 @@ fn extract_pubky(req: &Request) -> Option { if is_prefixed_pubky(s) { continue; } - if let Ok(key) = PublicKey::try_from(s) { + if let Ok(key) = PublicKey::try_from_z32(s) { pubky = Some(key); } } @@ -77,7 +77,7 @@ fn extract_pubky(req: &Request) -> Option { if is_prefixed_pubky(val) { return None; } - return PublicKey::try_from(val).ok(); + return PublicKey::try_from_z32(val).ok(); } } None @@ -86,7 +86,3 @@ fn extract_pubky(req: &Request) -> Option { } pubky } - -fn is_prefixed_pubky(value: &str) -> bool { - matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) -} diff --git a/pubky-homeserver/src/client_server/routes/auth.rs b/pubky-homeserver/src/client_server/routes/auth.rs index 56f4f277..716249b0 100644 --- a/pubky-homeserver/src/client_server/routes/auth.rs +++ b/pubky-homeserver/src/client_server/routes/auth.rs @@ -171,7 +171,7 @@ pub(crate) fn configure_session_cookie(cookie: &mut Cookie<'static>, host: &str) /// container names or localhost) are treated as non-secure development environments. fn is_secure(host: &str) -> bool { // A pkarr public key is always a secure context. - if PublicKey::try_from(host).is_ok() { + if PublicKey::try_from_z32(host).is_ok() { return true; } @@ -202,7 +202,7 @@ mod tests { assert!(!is_secure("[2001:0db8:0000:0000:0000:ff00:0042:8329]")); assert!(!is_secure("localhost")); assert!(!is_secure("localhost:23423")); - assert!(is_secure(&Keypair::random().public_key().to_string())); + assert!(is_secure(&Keypair::random().public_key().z32())); assert!(is_secure("example.com")); } } diff --git a/pubky-homeserver/src/client_server/routes/events.rs b/pubky-homeserver/src/client_server/routes/events.rs index 3f158e69..b01409cb 100644 --- a/pubky-homeserver/src/client_server/routes/events.rs +++ b/pubky-homeserver/src/client_server/routes/events.rs @@ -8,7 +8,7 @@ use axum::{ }, }; use futures_util::stream::Stream; -use pubky_common::crypto::PublicKey; +use pubky_common::crypto::{is_prefixed_pubky, PublicKey}; use serde::Deserialize; use std::{collections::HashMap, convert::Infallible, time::Instant}; use url::form_urlencoded; @@ -147,7 +147,10 @@ impl TryFrom for EventStreamQueryParams { (value.as_str(), None) }; - let pubkey = PublicKey::try_from(pubkey_str) + if is_prefixed_pubky(pubkey_str) { + return Err(EventStreamError::InvalidPublicKey(pubkey_str.to_string())); + } + let pubkey = PublicKey::try_from_z32(pubkey_str) .map_err(|_| EventStreamError::InvalidPublicKey(pubkey_str.to_string()))?; user_cursors.push((pubkey, cursor_str.map(|s| s.to_string()))); diff --git a/pubky-homeserver/src/persistence/files/events/events_entity.rs b/pubky-homeserver/src/persistence/files/events/events_entity.rs index 758222e8..8ae1c021 100644 --- a/pubky-homeserver/src/persistence/files/events/events_entity.rs +++ b/pubky-homeserver/src/persistence/files/events/events_entity.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use pubky_common::crypto::Hash; use pubky_common::crypto::PublicKey; use sea_query::Iden; @@ -38,10 +36,10 @@ impl FromRow<'_, PgRow> for EventEntity { let user_id: i32 = row.try_get(EventIden::User.to_string().as_str())?; let user_public_key: String = row.try_get(UserIden::PublicKey.to_string().as_str())?; let user_pubkey = - PublicKey::from_str(&user_public_key).map_err(|e| sqlx::Error::Decode(e.into()))?; + PublicKey::try_from_z32(&user_public_key).map_err(|e| sqlx::Error::Decode(e.into()))?; let event_type_str: String = row.try_get(EventIden::Type.to_string().as_str())?; let user_public_key = - PublicKey::from_str(&user_public_key).map_err(|e| sqlx::Error::Decode(e.into()))?; + PublicKey::try_from_z32(&user_public_key).map_err(|e| sqlx::Error::Decode(e.into()))?; let path: String = row.try_get(EventIden::Path.to_string().as_str())?; let path = WebDavPath::new(&path).map_err(|e| sqlx::Error::Decode(e.into()))?; let created_at: sqlx::types::chrono::NaiveDateTime = diff --git a/pubky-homeserver/src/persistence/files/user_quota_layer.rs b/pubky-homeserver/src/persistence/files/user_quota_layer.rs index d0039d23..b73ea898 100644 --- a/pubky-homeserver/src/persistence/files/user_quota_layer.rs +++ b/pubky-homeserver/src/persistence/files/user_quota_layer.rs @@ -393,11 +393,12 @@ mod tests { .await .expect_err("Should fail because the path doesn't start with a pubkey"); let pubkey = pubky_common::crypto::Keypair::random().public_key(); + let pubkey_raw = pubkey.z32(); UserRepository::create(&pubkey, &mut db.pool().into()) .await .unwrap(); operator - .write(format!("{}/test.txt", pubkey).as_str(), vec![0; 10]) + .write(format!("{}/test.txt", pubkey_raw).as_str(), vec![0; 10]) .await .expect("Should succeed because the path starts with a pubkey"); operator @@ -415,13 +416,17 @@ mod tests { let operator = get_memory_operator().layer(layer); let user_pubkey1 = pubky_common::crypto::Keypair::random().public_key(); + let user_pubkey1_raw = user_pubkey1.z32(); UserRepository::create(&user_pubkey1, &mut db.pool().into()) .await .unwrap(); // Write a file and see if the user usage is updated operator - .write(format!("{}/test.txt1", user_pubkey1).as_str(), vec![0; 10]) + .write( + format!("{}/test.txt1", user_pubkey1_raw).as_str(), + vec![0; 10], + ) .await .unwrap(); let user_usage = get_user_data_usage(&db, &user_pubkey1).await.unwrap(); @@ -429,7 +434,10 @@ mod tests { // Write the same file again but with a different size operator - .write(format!("{}/test.txt1", user_pubkey1).as_str(), vec![0; 12]) + .write( + format!("{}/test.txt1", user_pubkey1_raw).as_str(), + vec![0; 12], + ) .await .unwrap(); let user_usage = get_user_data_usage(&db, &user_pubkey1).await.unwrap(); @@ -437,7 +445,10 @@ mod tests { // Write a second file and see if the user usage is updated operator - .write(format!("{}/test.txt2", user_pubkey1).as_str(), vec![0; 5]) + .write( + format!("{}/test.txt2", user_pubkey1_raw).as_str(), + vec![0; 5], + ) .await .unwrap(); let user_usage = get_user_data_usage(&db, &user_pubkey1).await.unwrap(); @@ -445,7 +456,7 @@ mod tests { // Delete the first file and see if the user usage is updated operator - .delete(format!("{}/test.txt1", user_pubkey1).as_str()) + .delete(format!("{}/test.txt1", user_pubkey1_raw).as_str()) .await .unwrap(); let user_usage = get_user_data_usage(&db, &user_pubkey1).await.unwrap(); @@ -453,7 +464,7 @@ mod tests { // Delete the second file and see if the user usage is updated operator - .delete(format!("{}/test.txt2", user_pubkey1).as_str()) + .delete(format!("{}/test.txt2", user_pubkey1_raw).as_str()) .await .unwrap(); let user_usage = get_user_data_usage(&db, &user_pubkey1).await.unwrap(); @@ -479,11 +490,12 @@ mod tests { .layer(events_layer); let user_pubkey1 = pubky_common::crypto::Keypair::random().public_key(); + let user_pubkey1_raw = user_pubkey1.z32(); UserRepository::create(&user_pubkey1, &mut db.pool().into()) .await .unwrap(); - let file_name1 = format!("{}/test1.txt", user_pubkey1); + let file_name1 = format!("{}/test1.txt", user_pubkey1_raw); let entry_path1 = EntryPath::new(user_pubkey1.clone(), WebDavPath::new("/test1.txt").unwrap()); @@ -546,7 +558,7 @@ mod tests { "Event should be created after successful write" ); - let file_name2 = format!("{}/test2.txt", user_pubkey1); + let file_name2 = format!("{}/test2.txt", user_pubkey1_raw); // Write a second file and see if the user usage is updated operator .write(file_name2.as_str(), vec![0; 1]) diff --git a/pubky-homeserver/src/persistence/sql/entities/signup_code.rs b/pubky-homeserver/src/persistence/sql/entities/signup_code.rs index 0918ca4f..ee9bfae2 100644 --- a/pubky-homeserver/src/persistence/sql/entities/signup_code.rs +++ b/pubky-homeserver/src/persistence/sql/entities/signup_code.rs @@ -207,7 +207,9 @@ impl FromRow<'_, PgRow> for SignupCodeEntity { let used_by_raw: Option = row.try_get(SignupCodeIden::UsedBy.to_string().as_str())?; let used_by = used_by_raw - .map(|s| PublicKey::try_from(s.as_str()).map_err(|e| sqlx::Error::Decode(Box::new(e)))) + .map(|s| { + PublicKey::try_from_z32(s.as_str()).map_err(|e| sqlx::Error::Decode(Box::new(e))) + }) .transpose()?; Ok(SignupCodeEntity { id, diff --git a/pubky-homeserver/src/persistence/sql/entities/user.rs b/pubky-homeserver/src/persistence/sql/entities/user.rs index 6bac395d..df0a5d83 100644 --- a/pubky-homeserver/src/persistence/sql/entities/user.rs +++ b/pubky-homeserver/src/persistence/sql/entities/user.rs @@ -216,7 +216,7 @@ impl FromRow<'_, PgRow> for UserEntity { fn from_row(row: &PgRow) -> Result { let id: i32 = row.try_get(UserIden::Id.to_string().as_str())?; let raw_pubkey: String = row.try_get(UserIden::PublicKey.to_string().as_str())?; - let public_key = PublicKey::try_from(raw_pubkey.as_str()) + let public_key = PublicKey::try_from_z32(raw_pubkey.as_str()) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; let disabled: bool = row.try_get(UserIden::Disabled.to_string().as_str())?; let raw_used_bytes: i64 = row.try_get(UserIden::UsedBytes.to_string().as_str())?; diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs b/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs index 169580b4..bc511358 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20250806_create_user.rs @@ -99,7 +99,7 @@ mod tests { fn from_row(row: &PgRow) -> Result { let id: i32 = row.try_get(User::Id.to_string().as_str())?; let raw_pubkey: String = row.try_get(User::PublicKey.to_string().as_str())?; - let public_key = PublicKey::try_from(raw_pubkey.as_str()) + let public_key = PublicKey::try_from_z32(raw_pubkey.as_str()) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; let disabled: bool = row.try_get(User::Disabled.to_string().as_str())?; let raw_used_bytes: i64 = row.try_get(User::UsedBytes.to_string().as_str())?; diff --git a/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs b/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs index 8d8f1423..7666ed09 100644 --- a/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs +++ b/pubky-homeserver/src/persistence/sql/migrations/m20250812_create_signup_code.rs @@ -77,7 +77,8 @@ mod tests { row.try_get(SignupCodeIden::UsedBy.to_string().as_str())?; let used_by = used_by_raw .map(|s| { - PublicKey::try_from(s.as_str()).map_err(|e| sqlx::Error::Decode(Box::new(e))) + PublicKey::try_from_z32(s.as_str()) + .map_err(|e| sqlx::Error::Decode(Box::new(e))) }) .transpose()?; Ok(SignupCodeEntity { diff --git a/pubky-homeserver/src/shared/pubkey_path_validator.rs b/pubky-homeserver/src/shared/pubkey_path_validator.rs index 07d43c8a..01e63194 100644 --- a/pubky-homeserver/src/shared/pubkey_path_validator.rs +++ b/pubky-homeserver/src/shared/pubkey_path_validator.rs @@ -1,4 +1,4 @@ -use pubky_common::crypto::PublicKey; +use pubky_common::crypto::{is_prefixed_pubky, PublicKey}; /// Custom validator for the zbase32 pubkey in the route path. /// Usage: @@ -32,11 +32,7 @@ impl<'de> serde::Deserialize<'de> for Z32Pubkey { "unexpected `pubky` prefix; expected raw z32", )); } - let pubkey = PublicKey::try_from(s.as_str()).map_err(serde::de::Error::custom)?; + let pubkey = PublicKey::try_from_z32(s.as_str()).map_err(serde::de::Error::custom)?; Ok(Z32Pubkey(pubkey)) } } - -fn is_prefixed_pubky(value: &str) -> bool { - matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) -} diff --git a/pubky-homeserver/src/shared/webdav/entry_path.rs b/pubky-homeserver/src/shared/webdav/entry_path.rs index 2619cbfc..f6fca3ef 100644 --- a/pubky-homeserver/src/shared/webdav/entry_path.rs +++ b/pubky-homeserver/src/shared/webdav/entry_path.rs @@ -1,4 +1,4 @@ -use pubky_common::crypto::PublicKey; +use pubky_common::crypto::{is_prefixed_pubky, PublicKey}; use std::str::FromStr; use super::WebDavPath; @@ -70,7 +70,12 @@ impl FromStr for EntryPath { Some((pubkey, path)) => (pubkey, path), None => return Err(EntryPathError::Invalid("Missing '/'".to_string())), }; - let pubkey = PublicKey::from_str(pubkey).map_err(EntryPathError::InvalidPubkey)?; + if is_prefixed_pubky(pubkey) { + return Err(EntryPathError::Invalid( + "unexpected `pubky` prefix; expected raw z32".to_string(), + )); + } + let pubkey = PublicKey::try_from_z32(pubkey).map_err(EntryPathError::InvalidPubkey)?; let webdav_path = WebDavPath::new(path).map_err(EntryPathError::InvalidWebdavPath)?; Ok(Self::new(pubkey, webdav_path)) } diff --git a/pubky-sdk/bindings/js/src/wrappers/keys.rs b/pubky-sdk/bindings/js/src/wrappers/keys.rs index 2149d12a..e5bd8d42 100644 --- a/pubky-sdk/bindings/js/src/wrappers/keys.rs +++ b/pubky-sdk/bindings/js/src/wrappers/keys.rs @@ -1,8 +1,9 @@ use wasm_bindgen::prelude::*; -use crate::js_error::JsResult; +use crate::js_error::{JsResult, PubkyError, PubkyErrorName}; use js_sys::Uint8Array; use pubky::{Keypair as NativeKeypair, PublicKey as NativePublicKey}; +use pubky_common::crypto::is_prefixed_pubky; #[wasm_bindgen] pub struct Keypair(NativeKeypair); @@ -99,10 +100,16 @@ impl PublicKey { #[wasm_bindgen(js_name = "from")] /// @throws pub fn try_from(value: String) -> JsResult { - let value = value.strip_prefix("pubky://").unwrap_or(&value); - let value = match value.strip_prefix("pubky") { - Some(stripped) if stripped.len() == 52 => stripped, - _ => value, + if value.starts_with("pubky://") { + return Err(PubkyError::new( + PubkyErrorName::InvalidInput, + "public key must be raw z32 or pubky; pubky:// is not supported", + )); + } + let value = if is_prefixed_pubky(value) { + value.strip_prefix("pubky").unwrap_or(value) + } else { + value }; let native_pk = NativePublicKey::try_from(value)?; Ok(PublicKey(native_pk)) diff --git a/pubky-sdk/src/actors/auth/deep_links/signup.rs b/pubky-sdk/src/actors/auth/deep_links/signup.rs index 4a64268a..e17c8be0 100644 --- a/pubky-sdk/src/actors/auth/deep_links/signup.rs +++ b/pubky-sdk/src/actors/auth/deep_links/signup.rs @@ -146,7 +146,7 @@ impl FromStr for SignupDeepLink { .ok_or(DeepLinkParseError::MissingQueryParameter("hs"))? .1 .to_string(); - let homeserver = PublicKey::try_from(raw_homeserver.as_str()) + let homeserver = PublicKey::try_from_z32(raw_homeserver.as_str()) .map_err(|e| DeepLinkParseError::InvalidQueryParameter("hs", Box::new(e)))?; let signup_token = url diff --git a/pubky-sdk/src/actors/pkdns.rs b/pubky-sdk/src/actors/pkdns.rs index e0ba05a2..720a01cc 100644 --- a/pubky-sdk/src/actors/pkdns.rs +++ b/pubky-sdk/src/actors/pkdns.rs @@ -151,7 +151,7 @@ impl Pkdns { ); let packet = self.client.pkarr().resolve(user_public_key).await?; let s = extract_host_from_packet(&packet)?; - let result = PublicKey::try_from(s).ok(); + let result = PublicKey::try_from_z32(&s).ok(); cross_log!( debug, "Homeserver resolution for {} yielded {:?}", diff --git a/pubky-sdk/src/actors/session/persist.rs b/pubky-sdk/src/actors/session/persist.rs index ebfa7a2d..4933a1d4 100644 --- a/pubky-sdk/src/actors/session/persist.rs +++ b/pubky-sdk/src/actors/session/persist.rs @@ -50,9 +50,10 @@ impl PubkySession { message: "invalid secret: expected `:`".into(), })?; - let public_key = PublicKey::try_from(pk_str).map_err(|_err| RequestError::Validation { - message: "invalid public key".into(), - })?; + let public_key = + PublicKey::try_from_z32(pk_str).map_err(|_err| RequestError::Validation { + message: "invalid public key".into(), + })?; cross_log!(info, "Importing session secret for {}", public_key); // 3) Build minimal session; placeholder SessionInfo will be replaced after validation. diff --git a/pubky-sdk/src/actors/storage/resource.rs b/pubky-sdk/src/actors/storage/resource.rs index b1298b2e..9d8f2b3c 100644 --- a/pubky-sdk/src/actors/storage/resource.rs +++ b/pubky-sdk/src/actors/storage/resource.rs @@ -21,6 +21,7 @@ use std::{fmt, str::FromStr}; use crate::PublicKey; +use pubky_common::crypto::is_prefixed_pubky; use url::Url; use crate::{Error, errors::RequestError}; @@ -33,11 +34,6 @@ fn invalid(msg: impl Into) -> Error { .into() } -#[inline] -fn is_prefixed_pubky(value: &str) -> bool { - matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) -} - // ============================================================================ // ResourcePath // ============================================================================ @@ -249,7 +245,7 @@ impl PubkyResource { "transport URL host must use raw z32 without `pubky` prefix", )); } - let public_key = PublicKey::try_from(owner) + let public_key = PublicKey::try_from_z32(owner) .map_err(|_err| invalid("transport URL host does not contain a valid public key"))?; let path = if url.path().is_empty() { @@ -281,7 +277,7 @@ impl FromStr for PubkyResource { "unexpected `pubky` prefix in user id; use raw z32 after `pubky://`", )); } - let user = PublicKey::try_from(user_str) + let user = PublicKey::try_from_z32(user_str) .map_err(|_err| invalid(format!("invalid user public key: {user_str}")))?; return Self::new(user, path); } @@ -294,7 +290,7 @@ impl FromStr for PubkyResource { "unexpected `pubky` prefix in user id; use raw z32 after `pubky`", )); } - let user = PublicKey::try_from(user_id).map_err(|_err| { + let user = PublicKey::try_from_z32(user_id).map_err(|_err| { invalid("expected `pubky/` or `pubky:///`") })?; return Self::new(user, path); diff --git a/pubky-sdk/src/client/http_targets/native.rs b/pubky-sdk/src/client/http_targets/native.rs index b3ac0b42..d030a5b0 100644 --- a/pubky-sdk/src/client/http_targets/native.rs +++ b/pubky-sdk/src/client/http_targets/native.rs @@ -1,5 +1,6 @@ use crate::errors::RequestError; use crate::{PubkyHttpClient, PublicKey, Result, cross_log}; +use pubky_common::crypto::is_prefixed_pubky; use reqwest::{IntoUrl, Method, RequestBuilder}; use url::Url; @@ -12,22 +13,18 @@ enum HostKind { fn classify_host(host: &str) -> HostKind { if let Some(pk_host) = host.strip_prefix("_pubky.") { - if is_prefixed_pubky_host(pk_host) { + if is_prefixed_pubky(pk_host) { return HostKind::Icann; } - if PublicKey::try_from(pk_host).is_ok() { + if PublicKey::try_from_z32(pk_host).is_ok() { return HostKind::ResolvedPubky; } - } else if is_prefixed_pubky_host(host) || PublicKey::try_from(host).is_err() { + } else if is_prefixed_pubky(host) || PublicKey::try_from_z32(host).is_err() { return HostKind::Icann; } HostKind::Pubky } -fn is_prefixed_pubky_host(value: &str) -> bool { - matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) -} - impl PubkyHttpClient { /// Constructs a [`reqwest::RequestBuilder`] for the given HTTP `method` and `url`, /// routing through the client’s unified request path. @@ -48,7 +45,12 @@ impl PubkyHttpClient { clippy::unused_async, reason = "native implementation stays async to share the same signature as the WASM backend" )] - pub(crate) async fn cross_request(&self, method: Method, url: Url) -> Result { + pub(crate) async fn cross_request( + &self, + method: Method, + mut url: Url, + ) -> Result { + let _ = self.prepare_request(&mut url).await?; Ok(self.request(method, &url)) } @@ -67,25 +69,25 @@ impl PubkyHttpClient { let host = url.host_str().unwrap_or(""); if let Some(stripped) = host.strip_prefix("_pubky.") { - if is_prefixed_pubky_host(stripped) { + if is_prefixed_pubky(stripped) { return Err(RequestError::Validation { message: "pubky prefix is not allowed in transport hosts; use raw z32" .to_string(), } .into()); } - if PublicKey::try_from(stripped).is_ok() { + if PublicKey::try_from_z32(stripped).is_ok() { return Ok(Some(stripped.to_string())); } } else { - if is_prefixed_pubky_host(host) { + if is_prefixed_pubky(host) { return Err(RequestError::Validation { message: "pubky prefix is not allowed in transport hosts; use raw z32" .to_string(), } .into()); } - if PublicKey::try_from(host).is_ok() { + if PublicKey::try_from_z32(host).is_ok() { return Ok(Some(host.to_string())); } } diff --git a/pubky-sdk/src/client/http_targets/wasm.rs b/pubky-sdk/src/client/http_targets/wasm.rs index c6e1d337..7593eacd 100644 --- a/pubky-sdk/src/client/http_targets/wasm.rs +++ b/pubky-sdk/src/client/http_targets/wasm.rs @@ -5,6 +5,7 @@ use crate::errors::{PkarrError, RequestError, Result}; use crate::{PubkyHttpClient, cross_log}; use futures_lite::StreamExt; use pkarr::extra::endpoints::Endpoint; +use pubky_common::crypto::is_prefixed_pubky; use reqwest::{IntoUrl, Method, RequestBuilder}; use url::Url; @@ -42,33 +43,29 @@ impl PubkyHttpClient { pub async fn prepare_request(&self, url: &mut Url) -> Result> { let host = url.host_str().unwrap_or("").to_string(); - let invalid_prefixed_host = |value: &str| -> bool { - matches!(value.strip_prefix("pubky"), Some(stripped) if stripped.len() == 52) - }; - let mut pubky_host = None; if let Some(stripped) = host.strip_prefix("_pubky.") { - if invalid_prefixed_host(stripped) { + if is_prefixed_pubky(stripped) { return Err(RequestError::Validation { message: "pubky prefix is not allowed in transport hosts; use raw z32" .to_string(), } .into()); } - if PublicKey::try_from(stripped).is_ok() { + if PublicKey::try_from_z32(stripped).is_ok() { self.transform_url(url).await?; pubky_host = Some(stripped.to_string()); } } else { - if invalid_prefixed_host(&host) { + if is_prefixed_pubky(&host) { return Err(RequestError::Validation { message: "pubky prefix is not allowed in transport hosts; use raw z32" .to_string(), } .into()); } - if PublicKey::try_from(host.clone()).is_ok() { + if PublicKey::try_from_z32(host.clone()).is_ok() { self.transform_url(url).await?; pubky_host = Some(host); } From e66af2e109eff762245502f308bfdf50448f2e1d Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Sun, 4 Jan 2026 04:44:34 +0100 Subject: [PATCH 16/21] use z32 on query params --- e2e/src/tests/metrics.rs | 6 ++++-- e2e/src/tests/storage.rs | 2 +- .../src/client_server/layers/rate_limiter/layer.rs | 8 ++++++-- pubky-sdk/src/client/http_targets/wasm.rs | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/e2e/src/tests/metrics.rs b/e2e/src/tests/metrics.rs index 65af3cc0..8c093864 100644 --- a/e2e/src/tests/metrics.rs +++ b/e2e/src/tests/metrics.rs @@ -130,11 +130,13 @@ async fn metrics_comprehensive() { // 3. Test concurrent stream connections to generate metrics let stream_url1 = format!( "https://{}/events-stream?user={}&live=true", - server_public_key_z32, user_pubky1 + server_public_key_z32, + user_pubky1.z32() ); let stream_url2 = format!( "https://{}/events-stream?user={}&live=true", - server_public_key_z32, user_pubky2 + server_public_key_z32, + user_pubky2.z32() ); let response1 = pubky diff --git a/e2e/src/tests/storage.rs b/e2e/src/tests/storage.rs index 73e580a5..17461ac6 100644 --- a/e2e/src/tests/storage.rs +++ b/e2e/src/tests/storage.rs @@ -48,7 +48,7 @@ async fn put_get_delete() { let regular_url = format!( "{}pub/foo.txt?pubky-host={}", server.icann_http_url(), - session.info().public_key() + session.info().public_key().z32() ); // We set `non.pubky.host` header as otherwise he client will use by default diff --git a/pubky-homeserver/src/client_server/layers/rate_limiter/layer.rs b/pubky-homeserver/src/client_server/layers/rate_limiter/layer.rs index 51d29915..335aa45e 100644 --- a/pubky-homeserver/src/client_server/layers/rate_limiter/layer.rs +++ b/pubky-homeserver/src/client_server/layers/rate_limiter/layer.rs @@ -358,7 +358,7 @@ mod tests { Router, }; use axum_server::Server; - use pkarr::{Keypair, PublicKey}; + use pubky_common::crypto::{Keypair, PublicKey}; use reqwest::{Client, Response}; use tokio::{task::JoinHandle, time::Instant}; @@ -544,7 +544,11 @@ mod tests { tokio::spawn(async move { let client = Client::new(); let response = client - .post(format!("http://{}/upload?pubky-host={user_pubkey}", socket)) + .post(format!( + "http://{}/upload?pubky-host={}", + socket, + user_pubkey.z32() + )) .send() .await .unwrap(); diff --git a/pubky-sdk/src/client/http_targets/wasm.rs b/pubky-sdk/src/client/http_targets/wasm.rs index 7593eacd..a6a457ee 100644 --- a/pubky-sdk/src/client/http_targets/wasm.rs +++ b/pubky-sdk/src/client/http_targets/wasm.rs @@ -65,7 +65,7 @@ impl PubkyHttpClient { } .into()); } - if PublicKey::try_from_z32(host.clone()).is_ok() { + if PublicKey::try_from_z32(&host).is_ok() { self.transform_url(url).await?; pubky_host = Some(host); } From a9eaff4dcedfe9e0c3a1520397617f21d7b1a3ed Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Sun, 4 Jan 2026 04:48:06 +0100 Subject: [PATCH 17/21] fix wasm clippy --- pubky-sdk/bindings/js/src/wrappers/keys.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pubky-sdk/bindings/js/src/wrappers/keys.rs b/pubky-sdk/bindings/js/src/wrappers/keys.rs index e5bd8d42..ae2643bf 100644 --- a/pubky-sdk/bindings/js/src/wrappers/keys.rs +++ b/pubky-sdk/bindings/js/src/wrappers/keys.rs @@ -106,8 +106,11 @@ impl PublicKey { "public key must be raw z32 or pubky; pubky:// is not supported", )); } - let value = if is_prefixed_pubky(value) { - value.strip_prefix("pubky").unwrap_or(value) + let value = if is_prefixed_pubky(&value) { + value + .strip_prefix("pubky") + .unwrap_or(value.as_str()) + .to_string() } else { value }; From 5b31093c9f3da1507d5ab6067b1eaa5ffb201bfa Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Sun, 4 Jan 2026 05:17:56 +0100 Subject: [PATCH 18/21] fix e2e --- e2e/src/tests/auth.rs | 6 +- e2e/src/tests/storage.rs | 202 ++++++++++++++++----------------------- 2 files changed, 88 insertions(+), 120 deletions(-) diff --git a/e2e/src/tests/auth.rs b/e2e/src/tests/auth.rs index f6e0c2a3..3fe18211 100644 --- a/e2e/src/tests/auth.rs +++ b/e2e/src/tests/auth.rs @@ -39,7 +39,7 @@ async fn disabled_user() { // Create a brand-new user and session let signer = pubky.signer(Keypair::random()); - let pubky = signer.public_key().clone(); + let user_pubky = signer.public_key().clone(); let session = signer.signup(&server.public_key(), None).await.unwrap(); // Create a test file to ensure the user can write to their account @@ -69,7 +69,7 @@ async fn disabled_user() { let response = admin_client .request( Method::POST, - &format!("http://{admin_socket}/users/{pubky}/disable"), + &format!("http://{admin_socket}/users/{}/disable", user_pubky.z32()), ) .header("X-Admin-Password", "admin") .send() @@ -99,7 +99,7 @@ async fn disabled_user() { .signin() .await .expect("Signin should succeed for disabled users"); - assert_eq!(session2.info().public_key(), &pubky); + assert_eq!(session2.info().public_key(), &user_pubky); } #[tokio::test] diff --git a/e2e/src/tests/storage.rs b/e2e/src/tests/storage.rs index 17461ac6..2b8c94f4 100644 --- a/e2e/src/tests/storage.rs +++ b/e2e/src/tests/storage.rs @@ -1,12 +1,56 @@ use bytes::Bytes; use pubky_testnet::{ - pubky::{errors::RequestError, Error, IntoPubkyResource, Keypair, Method, StatusCode}, + pubky::{ + errors::RequestError, Error, IntoPubkyResource, Keypair, Method, PubkyHttpClient, + StatusCode, + }, pubky_homeserver::MockDataDir, EphemeralTestnet, Testnet, }; use rand::rng; use rand::seq::SliceRandom; +async fn collect_feed_events( + client: &PubkyHttpClient, + feed_url: &str, + limit: usize, + user_ids: &[String], + expected: usize, +) -> Vec { + let mut cursor: Option = None; + let mut out = Vec::new(); + + loop { + let url = match cursor.as_ref() { + Some(cursor) => format!("{feed_url}?limit={limit}&cursor={cursor}"), + None => format!("{feed_url}?limit={limit}"), + }; + let resp = client.request(Method::GET, &url).send().await.unwrap(); + let text = resp.text().await.unwrap(); + let mut next_cursor = None; + + for line in text.lines() { + if let Some(value) = line.strip_prefix("cursor: ") { + next_cursor = Some(value.to_string()); + continue; + } + if user_ids.iter().any(|user_id| line.contains(user_id)) { + out.push(line.to_string()); + if out.len() >= expected { + return out; + } + } + } + + match next_cursor { + Some(next_cursor) => cursor = Some(next_cursor), + None => break, + } + } + + out +} + #[tokio::test] #[pubky_testnet::test] async fn put_get_delete() { @@ -292,6 +336,7 @@ async fn list_deep() { let owner = pubky.signer(Keypair::random()); let owner_session = owner.signup(&server.public_key(), None).await.unwrap(); let public_key = owner_session.info().public_key(); + let public_key_z32 = public_key.z32(); // Write files to the server let mut paths = vec![ format!("/pub/a.wrong/a.txt"), @@ -327,10 +372,10 @@ async fn list_deep() { format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/c.txt") + format!("{public_key}/pub/example.com/cc-nested/z.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/cc-nested/z.txt") + format!("{public_key}/pub/example.com/c.txt") .parse() .unwrap(), format!("{public_key}/pub/example.com/d.txt") @@ -372,7 +417,7 @@ async fn list_deep() { .list(&url) .unwrap() .limit(2) - .cursor(format!("{public_key}/pub/example.com/a.txt").as_str()) + .cursor(format!("{public_key_z32}/pub/example.com/a.txt").as_str()) .send() .await .unwrap(); @@ -383,7 +428,7 @@ async fn list_deep() { format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/c.txt") + format!("{public_key}/pub/example.com/cc-nested/z.txt") .parse() .unwrap(), ], @@ -397,7 +442,7 @@ async fn list_deep() { .list(&url) .unwrap() .limit(2) - .cursor(&format!("{public_key}/pub/example.com/a.txt")) + .cursor(&format!("{public_key_z32}/pub/example.com/a.txt")) .send() .await .unwrap(); @@ -408,7 +453,7 @@ async fn list_deep() { format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/c.txt") + format!("{public_key}/pub/example.com/cc-nested/z.txt") .parse() .unwrap(), ], @@ -428,6 +473,7 @@ async fn list_shallow() { let owner = pubky.signer(Keypair::random()); let owner_session = owner.signup(&server.public_key(), None).await.unwrap(); let public_key = owner_session.info().public_key(); + let public_key_z32 = public_key.z32(); // Write files to the server let mut urls = vec![ @@ -503,7 +549,7 @@ async fn list_shallow() { .unwrap() .shallow(true) .limit(2) - .cursor(format!("{public_key}/pub/example.com/").as_str()) + .cursor(format!("{public_key_z32}/pub/example.com/").as_str()) .send() .await .unwrap(); @@ -523,7 +569,7 @@ async fn list_shallow() { .unwrap() .shallow(true) .limit(2) - .cursor(format!("{public_key}/pub/example.com/a.txt").as_str()) + .cursor(format!("{public_key_z32}/pub/example.com/a.txt").as_str()) .send() .await .unwrap(); @@ -541,7 +587,7 @@ async fn list_shallow() { .unwrap() .shallow(true) .limit(3) - .cursor(format!("{public_key}/pub/example.com/").as_str()) + .cursor(format!("{public_key_z32}/pub/example.com/").as_str()) .send() .await .unwrap(); @@ -592,78 +638,24 @@ async fn list_events() { // Feed is exposed under the public-key host let feed_url = format!("https://{}/events/", server.public_key().z32()); - // Page 1 - let cursor: String = { - let page1_url = format!("{feed_url}?limit=10"); - let resp = session - .client() - .request(Method::GET, &page1_url) - .send() - .await - .unwrap(); - - let text = resp.text().await.unwrap(); - let lines = text.split('\n').collect::>(); - - // last line is "cursor: " - let cursor = lines - .last() - .unwrap() - .rsplit(' ') - .next() - .unwrap() - .to_string(); - - assert_eq!( - lines, - vec![ - format!("PUT pubky://{public_key_z32}/pub/a.com/a.txt"), - format!("DEL pubky://{public_key_z32}/pub/a.com/a.txt"), - format!("PUT pubky://{public_key_z32}/pub/example.com/a.txt"), - format!("DEL pubky://{public_key_z32}/pub/example.com/a.txt"), - format!("PUT pubky://{public_key_z32}/pub/example.com/b.txt"), - format!("DEL pubky://{public_key_z32}/pub/example.com/b.txt"), - format!("PUT pubky://{public_key_z32}/pub/example.com/c.txt"), - format!("DEL pubky://{public_key_z32}/pub/example.com/c.txt"), - format!("PUT pubky://{public_key_z32}/pub/example.com/d.txt"), - format!("DEL pubky://{public_key_z32}/pub/example.com/d.txt"), - format!("cursor: {cursor}"), - ] - ); - - cursor - }; - - // Page 2 (using cursor) - { - let page2_url = format!("{feed_url}?limit=10&cursor={cursor}"); - let resp = session - .client() - .request(Method::GET, &page2_url) - .send() - .await - .unwrap(); - - let text = resp.text().await.unwrap(); - let lines = text.split('\n').collect::>(); - - assert_eq!( - lines, - vec![ - format!("PUT pubky://{public_key_z32}/pub/example.xyz/d.txt"), - format!("DEL pubky://{public_key_z32}/pub/example.xyz/d.txt"), - format!("PUT pubky://{public_key_z32}/pub/example.xyz"), - format!("DEL pubky://{public_key_z32}/pub/example.xyz"), - format!("PUT pubky://{public_key_z32}/pub/file"), - format!("DEL pubky://{public_key_z32}/pub/file"), - format!("PUT pubky://{public_key_z32}/pub/file2"), - format!("DEL pubky://{public_key_z32}/pub/file2"), - format!("PUT pubky://{public_key_z32}/pub/z.com/a.txt"), - format!("DEL pubky://{public_key_z32}/pub/z.com/a.txt"), - lines.last().unwrap().to_string(), + let expected = paths + .iter() + .flat_map(|path| { + [ + format!("PUT pubky://{public_key_z32}{path}"), + format!("DEL pubky://{public_key_z32}{path}"), ] - ); - } + }) + .collect::>(); + let events = collect_feed_events( + &session.client(), + &feed_url, + 10, + &[public_key_z32.clone()], + expected.len(), + ) + .await; + assert_eq!(events, expected); } #[tokio::test] @@ -690,28 +682,9 @@ async fn read_after_event() { // Events page 1 let feed_url = format!("https://{}/events/", server.public_key().z32()); { - let page_url = format!("{feed_url}?limit=10"); - let resp = pubky - .client() - .request(Method::GET, &page_url) - .send() - .await - .unwrap(); - - let text = resp.text().await.unwrap(); - let lines = text.split('\n').collect::>(); - let cursor = lines - .last() - .unwrap() - .rsplit(' ') - .next() - .unwrap() - .to_string(); - - assert_eq!( - lines, - vec![format!("PUT {url}"), format!("cursor: {cursor}")] - ); + let events = + collect_feed_events(&pubky.client(), &feed_url, 10, &[public_key_z32.clone()], 1).await; + assert_eq!(events, vec![format!("PUT {url}")]); } // Now the file should exist @@ -760,25 +733,20 @@ async fn dont_delete_shared_blobs() { // Event feed should show PUT u1, PUT u2, DEL u1 (order preserved) let feed_url = format!("https://{}/events/", homeserver.public_key().z32()); - let resp = pubky - .client() - .request(Method::GET, &feed_url) - .send() - .await - .unwrap() - .error_for_status() - .unwrap(); - - let text = resp.text().await.unwrap(); - let lines = text.split('\n').collect::>(); - + let events = collect_feed_events( + &pubky.client(), + &feed_url, + 10, + &[user_1_id.z32(), user_2_id.z32()], + 3, + ) + .await; assert_eq!( - lines, + events, vec![ format!("PUT pubky://{}/pub/pubky.app/file/file_1", user_1_id.z32()), format!("PUT pubky://{}/pub/pubky.app/file/file_1", user_2_id.z32()), format!("DEL pubky://{}/pub/pubky.app/file/file_1", user_1_id.z32()), - lines.last().unwrap().to_string(), ] ); } From 5dac082ad06ba1d06bd440284f84c4ebd2801782 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 8 Jan 2026 09:26:03 +0100 Subject: [PATCH 19/21] fix admin tests for pubky keys --- e2e/src/tests/admin.rs | 5 ++--- pubky-homeserver/src/admin_server/app.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/e2e/src/tests/admin.rs b/e2e/src/tests/admin.rs index a50277e5..127133a4 100644 --- a/e2e/src/tests/admin.rs +++ b/e2e/src/tests/admin.rs @@ -1,5 +1,4 @@ -use pkarr::Keypair; -use pubky_testnet::pubky::{Method, PubkyHttpClient, StatusCode}; +use pubky_testnet::pubky::{Keypair, Method, PubkyHttpClient, StatusCode}; use pubky_testnet::{ pubky_homeserver::{ConfigToml, Domain, MockDataDir}, Testnet, @@ -71,7 +70,7 @@ async fn admin_info_includes_metadata() { assert_eq!(response.status(), StatusCode::OK); let body: InfoResponse = response.json().await.unwrap(); - assert_eq!(body.public_key, homeserver.public_key().to_string()); + assert_eq!(body.public_key, homeserver.public_key().z32()); assert_eq!(body.pkarr_pubky_address, Some(expected_pubky_endpoint)); assert_eq!(body.pkarr_icann_domain, Some(expected_icann_endpoint)); assert_eq!(body.version, env!("CARGO_PKG_VERSION")); diff --git a/pubky-homeserver/src/admin_server/app.rs b/pubky-homeserver/src/admin_server/app.rs index 15a40b92..902b6da2 100644 --- a/pubky-homeserver/src/admin_server/app.rs +++ b/pubky-homeserver/src/admin_server/app.rs @@ -110,7 +110,7 @@ impl AdminServer { &password, ) .with_metadata_from_config( - context.keypair.public_key().to_string(), + context.keypair.public_key().z32(), &context.config_toml, env!("CARGO_PKG_VERSION"), ); From ac61098bc6039ed652513b6bc3ed4abb41309c3e Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 8 Jan 2026 09:57:02 +0100 Subject: [PATCH 20/21] aling expected e2e order/cursor to collate C expectations --- e2e/src/tests/storage.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/src/tests/storage.rs b/e2e/src/tests/storage.rs index 2b8c94f4..55989fef 100644 --- a/e2e/src/tests/storage.rs +++ b/e2e/src/tests/storage.rs @@ -372,10 +372,10 @@ async fn list_deep() { format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/cc-nested/z.txt") + format!("{public_key}/pub/example.com/c.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/c.txt") + format!("{public_key}/pub/example.com/cc-nested/z.txt") .parse() .unwrap(), format!("{public_key}/pub/example.com/d.txt") @@ -428,7 +428,7 @@ async fn list_deep() { format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/cc-nested/z.txt") + format!("{public_key}/pub/example.com/c.txt") .parse() .unwrap(), ], @@ -453,7 +453,7 @@ async fn list_deep() { format!("{public_key}/pub/example.com/b.txt") .parse() .unwrap(), - format!("{public_key}/pub/example.com/cc-nested/z.txt") + format!("{public_key}/pub/example.com/c.txt") .parse() .unwrap(), ], From f7cdd4577de1e59b7e0313830cb565279d407ae3 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 8 Jan 2026 10:24:15 +0100 Subject: [PATCH 21/21] revert e2e storage utils --- e2e/src/tests/storage.rs | 190 +++++++++++++++++++++------------------ 1 file changed, 105 insertions(+), 85 deletions(-) diff --git a/e2e/src/tests/storage.rs b/e2e/src/tests/storage.rs index 55989fef..896684ed 100644 --- a/e2e/src/tests/storage.rs +++ b/e2e/src/tests/storage.rs @@ -1,56 +1,12 @@ use bytes::Bytes; use pubky_testnet::{ - pubky::{ - errors::RequestError, Error, IntoPubkyResource, Keypair, Method, PubkyHttpClient, - StatusCode, - }, + pubky::{errors::RequestError, Error, IntoPubkyResource, Keypair, Method, StatusCode}, pubky_homeserver::MockDataDir, EphemeralTestnet, Testnet, }; use rand::rng; use rand::seq::SliceRandom; -async fn collect_feed_events( - client: &PubkyHttpClient, - feed_url: &str, - limit: usize, - user_ids: &[String], - expected: usize, -) -> Vec { - let mut cursor: Option = None; - let mut out = Vec::new(); - - loop { - let url = match cursor.as_ref() { - Some(cursor) => format!("{feed_url}?limit={limit}&cursor={cursor}"), - None => format!("{feed_url}?limit={limit}"), - }; - let resp = client.request(Method::GET, &url).send().await.unwrap(); - let text = resp.text().await.unwrap(); - let mut next_cursor = None; - - for line in text.lines() { - if let Some(value) = line.strip_prefix("cursor: ") { - next_cursor = Some(value.to_string()); - continue; - } - if user_ids.iter().any(|user_id| line.contains(user_id)) { - out.push(line.to_string()); - if out.len() >= expected { - return out; - } - } - } - - match next_cursor { - Some(next_cursor) => cursor = Some(next_cursor), - None => break, - } - } - - out -} - #[tokio::test] #[pubky_testnet::test] async fn put_get_delete() { @@ -77,7 +33,7 @@ async fn put_get_delete() { // Use Pubky native method to get data from homeserver let response = pubky .public_storage() - .get(format!("{public_key}/{}", path.trim_start_matches('/'))) + .get(format!("{public_key}/{path}")) .await .unwrap(); @@ -159,7 +115,7 @@ async fn put_then_get_json_roundtrip() { // Read back as strongly-typed JSON and assert equality. let got: Payload = pubky .public_storage() - .get_json(format!("{}/{}", public_key, path.trim_start_matches('/'))) + .get_json(format!("{public_key}/{path}")) .await .unwrap(); assert_eq!(got, expected); @@ -336,7 +292,6 @@ async fn list_deep() { let owner = pubky.signer(Keypair::random()); let owner_session = owner.signup(&server.public_key(), None).await.unwrap(); let public_key = owner_session.info().public_key(); - let public_key_z32 = public_key.z32(); // Write files to the server let mut paths = vec![ format!("/pub/a.wrong/a.txt"), @@ -354,7 +309,7 @@ async fn list_deep() { } // List all files with no cursor, no limit - let url = "/pub/example.com/".to_string(); + let url = format!("/pub/example.com/"); { let list = owner_session .storage() @@ -417,7 +372,7 @@ async fn list_deep() { .list(&url) .unwrap() .limit(2) - .cursor(format!("{public_key_z32}/pub/example.com/a.txt").as_str()) + .cursor(format!("{}/pub/example.com/a.txt", public_key.z32()).as_str()) .send() .await .unwrap(); @@ -442,7 +397,7 @@ async fn list_deep() { .list(&url) .unwrap() .limit(2) - .cursor(&format!("{public_key_z32}/pub/example.com/a.txt")) + .cursor(&format!("{}/pub/example.com/a.txt", public_key.z32())) .send() .await .unwrap(); @@ -473,7 +428,6 @@ async fn list_shallow() { let owner = pubky.signer(Keypair::random()); let owner_session = owner.signup(&server.public_key(), None).await.unwrap(); let public_key = owner_session.info().public_key(); - let public_key_z32 = public_key.z32(); // Write files to the server let mut urls = vec![ @@ -494,7 +448,7 @@ async fn list_shallow() { } // List all files with no cursor, no limit - let url = "/pub/".to_string(); + let url = format!("/pub/"); { let list = owner_session .storage() @@ -549,7 +503,7 @@ async fn list_shallow() { .unwrap() .shallow(true) .limit(2) - .cursor(format!("{public_key_z32}/pub/example.com/").as_str()) + .cursor(format!("{}/pub/example.com/", public_key.z32()).as_str()) .send() .await .unwrap(); @@ -569,7 +523,7 @@ async fn list_shallow() { .unwrap() .shallow(true) .limit(2) - .cursor(format!("{public_key_z32}/pub/example.com/a.txt").as_str()) + .cursor(format!("{}/pub/example.com/a.txt", public_key.z32()).as_str()) .send() .await .unwrap(); @@ -587,7 +541,7 @@ async fn list_shallow() { .unwrap() .shallow(true) .limit(3) - .cursor(format!("{public_key_z32}/pub/example.com/").as_str()) + .cursor(format!("{}/pub/example.com/", public_key.z32()).as_str()) .send() .await .unwrap(); @@ -638,24 +592,72 @@ async fn list_events() { // Feed is exposed under the public-key host let feed_url = format!("https://{}/events/", server.public_key().z32()); - let expected = paths - .iter() - .flat_map(|path| { - [ - format!("PUT pubky://{public_key_z32}{path}"), - format!("DEL pubky://{public_key_z32}{path}"), + // Page 1 + let cursor: String = { + let page1_url = format!("{feed_url}?limit=10"); + let resp = session + .client() + .request(Method::GET, &page1_url) + .send() + .await + .unwrap(); + + let text = resp.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + // last line is "cursor: " + let cursor = lines.last().unwrap().split(' ').last().unwrap().to_string(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{public_key_z32}/pub/a.com/a.txt"), + format!("DEL pubky://{public_key_z32}/pub/a.com/a.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/a.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/a.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/b.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/b.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/c.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/c.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.com/d.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.com/d.txt"), + format!("cursor: {cursor}"), + ] + ); + + cursor + }; + + // Page 2 (using cursor) + { + let page2_url = format!("{feed_url}?limit=10&cursor={cursor}"); + let resp = session + .client() + .request(Method::GET, &page2_url) + .send() + .await + .unwrap(); + + let text = resp.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{public_key_z32}/pub/example.xyz/d.txt"), + format!("DEL pubky://{public_key_z32}/pub/example.xyz/d.txt"), + format!("PUT pubky://{public_key_z32}/pub/example.xyz"), + format!("DEL pubky://{public_key_z32}/pub/example.xyz"), + format!("PUT pubky://{public_key_z32}/pub/file"), + format!("DEL pubky://{public_key_z32}/pub/file"), + format!("PUT pubky://{public_key_z32}/pub/file2"), + format!("DEL pubky://{public_key_z32}/pub/file2"), + format!("PUT pubky://{public_key_z32}/pub/z.com/a.txt"), + format!("DEL pubky://{public_key_z32}/pub/z.com/a.txt"), + lines.last().unwrap().to_string(), ] - }) - .collect::>(); - let events = collect_feed_events( - &session.client(), - &feed_url, - 10, - &[public_key_z32.clone()], - expected.len(), - ) - .await; - assert_eq!(events, expected); + ); + } } #[tokio::test] @@ -682,9 +684,22 @@ async fn read_after_event() { // Events page 1 let feed_url = format!("https://{}/events/", server.public_key().z32()); { - let events = - collect_feed_events(&pubky.client(), &feed_url, 10, &[public_key_z32.clone()], 1).await; - assert_eq!(events, vec![format!("PUT {url}")]); + let page_url = format!("{feed_url}?limit=10"); + let resp = pubky + .client() + .request(Method::GET, &page_url) + .send() + .await + .unwrap(); + + let text = resp.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 {url}"), format!("cursor: {cursor}")] + ); } // Now the file should exist @@ -733,20 +748,25 @@ async fn dont_delete_shared_blobs() { // Event feed should show PUT u1, PUT u2, DEL u1 (order preserved) let feed_url = format!("https://{}/events/", homeserver.public_key().z32()); - let events = collect_feed_events( - &pubky.client(), - &feed_url, - 10, - &[user_1_id.z32(), user_2_id.z32()], - 3, - ) - .await; + let resp = pubky + .client() + .request(Method::GET, &feed_url) + .send() + .await + .unwrap() + .error_for_status() + .unwrap(); + + let text = resp.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + assert_eq!( - events, + lines, vec![ format!("PUT pubky://{}/pub/pubky.app/file/file_1", user_1_id.z32()), format!("PUT pubky://{}/pub/pubky.app/file/file_1", user_2_id.z32()), format!("DEL pubky://{}/pub/pubky.app/file/file_1", user_1_id.z32()), + lines.last().unwrap().to_string(), ] ); }