From ce14d0633fc259bdd6fd4083ccb49a7c68ba2624 Mon Sep 17 00:00:00 2001 From: Nuh Date: Thu, 18 Jul 2024 09:27:07 +0300 Subject: [PATCH 001/125] Update README.md to add alpha warning --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99e127f..23197ae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # Pubky -Public key addressable web. +> The Web, long centralized, must decentralize; Long decentralized, must centralize. + +> [!WARNING] +> Pubky is still under heavy development and should be considered an alpha software. +> +> Features might be added, removed, or changed. Data might be lost. From 401872a61fa24ee89fc695e55c27d2b2a7b9aa4b Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 23 Jul 2024 11:26:12 +0300 Subject: [PATCH 002/125] feat(homeserver): stream incoming body --- Cargo.lock | 17 +++++ pubky-common/src/crypto.rs | 2 + pubky-homeserver/Cargo.toml | 4 +- pubky-homeserver/src/config.rs | 15 +++-- pubky-homeserver/src/database.rs | 15 +---- pubky-homeserver/src/database/migrations.rs | 20 +++--- .../src/database/migrations/m0.rs | 15 +++++ pubky-homeserver/src/database/tables.rs | 2 + pubky-homeserver/src/database/tables/blobs.rs | 13 ++++ .../src/database/tables/entries.rs | 66 +++++++++++++++++++ pubky-homeserver/src/routes.rs | 6 +- pubky-homeserver/src/routes/auth.rs | 7 +- pubky-homeserver/src/routes/drive.rs | 60 +++++++++++++++-- pubky-homeserver/src/server.rs | 4 +- 14 files changed, 207 insertions(+), 39 deletions(-) create mode 100644 pubky-homeserver/src/database/migrations/m0.rs create mode 100644 pubky-homeserver/src/database/tables/blobs.rs create mode 100644 pubky-homeserver/src/database/tables/entries.rs diff --git a/Cargo.lock b/Cargo.lock index 26bae45..74ef877 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,8 @@ dependencies = [ "mime", "pin-project-lite", "serde", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -1283,6 +1285,8 @@ dependencies = [ "base32", "bytes", "dirs-next", + "flume", + "futures-util", "heed", "pkarr", "postcard", @@ -1826,6 +1830,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.4.13" diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index 2f8131c..ec8f58a 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -8,6 +8,8 @@ pub type Hash = blake3::Hash; pub use blake3::hash; +pub use blake3::Hasher; + pub fn random_hash() -> Hash { let mut rng = rand::thread_rng(); Hash::from_bytes(rng.gen()) diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 33e18ab..da0c5c7 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -6,10 +6,12 @@ edition = "2021" [dependencies] anyhow = "1.0.82" axum = "0.7.5" -axum-extra = { version = "0.9.3", features = ["typed-header"] } +axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] } base32 = "0.5.1" bytes = "1.6.1" dirs-next = "2.0.0" +flume = "0.11.0" +futures-util = "0.3.30" heed = "0.20.3" pkarr = { version = "2.1.0", features = ["async"] } postcard = { version = "1.0.8", features = ["alloc"] } diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 7c9fcfe..3657ecd 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -40,13 +40,16 @@ impl Config { /// Test configurations pub fn test(testnet: &pkarr::mainline::Testnet) -> Self { + let bootstrap = Some(testnet.bootstrap.to_owned()); + let storage = Some( + std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join(DEFAULT_STORAGE_DIR), + ); + Self { - bootstrap: Some(testnet.bootstrap.to_owned()), - storage: Some( - std::env::temp_dir() - .join(Timestamp::now().to_string()) - .join(DEFAULT_STORAGE_DIR), - ), + bootstrap, + storage, ..Default::default() } } diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 2f8d591..0eb3200 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -19,21 +19,10 @@ impl DB { let env = unsafe { EnvOpenOptions::new().max_dbs(TABLES_COUNT).open(storage) }?; - let db = DB { env }; + migrations::run(&env); - db.run_migrations(); + let db = DB { env }; Ok(db) } - - fn run_migrations(&self) -> anyhow::Result<()> { - let mut wtxn = self.env.write_txn()?; - - migrations::create_users_table(&self.env, &mut wtxn); - migrations::create_sessions_table(&self.env, &mut wtxn); - - wtxn.commit()?; - - Ok(()) - } } diff --git a/pubky-homeserver/src/database/migrations.rs b/pubky-homeserver/src/database/migrations.rs index 93c7631..dbead07 100644 --- a/pubky-homeserver/src/database/migrations.rs +++ b/pubky-homeserver/src/database/migrations.rs @@ -1,19 +1,17 @@ use heed::{types::Str, Database, Env, RwTxn}; +mod m0; + use super::tables; -pub const TABLES_COUNT: u32 = 2; +pub const TABLES_COUNT: u32 = 4; -pub fn create_users_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { - let _: tables::users::UsersTable = - env.create_database(wtxn, Some(tables::users::USERS_TABLE))?; - - Ok(()) -} - -pub fn create_sessions_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { - let _: tables::sessions::SessionsTable = - env.create_database(wtxn, Some(tables::sessions::SESSIONS_TABLE))?; +pub fn run(env: &Env) -> anyhow::Result<()> { + let mut wtxn = env.write_txn()?; + + m0::run(env, &mut wtxn); + + wtxn.commit()?; Ok(()) } diff --git a/pubky-homeserver/src/database/migrations/m0.rs b/pubky-homeserver/src/database/migrations/m0.rs new file mode 100644 index 0000000..74d89c4 --- /dev/null +++ b/pubky-homeserver/src/database/migrations/m0.rs @@ -0,0 +1,15 @@ +use heed::{types::Str, Database, Env, RwTxn}; + +use super::tables::{blobs, entries, sessions, users}; + +pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { + let _: users::UsersTable = env.create_database(wtxn, Some(users::USERS_TABLE))?; + + let _: sessions::SessionsTable = env.create_database(wtxn, Some(sessions::SESSIONS_TABLE))?; + + let _: blobs::BlobsTable = env.create_database(wtxn, Some(blobs::BLOBS_TABLE))?; + + let _: entries::EntriesTable = env.create_database(wtxn, Some(entries::ENTRIES_TABLE))?; + + Ok(()) +} diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index b6e3efc..4f0c1c5 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -1,2 +1,4 @@ +pub mod blobs; +pub mod entries; pub mod sessions; pub mod users; diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs new file mode 100644 index 0000000..9cf1da1 --- /dev/null +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -0,0 +1,13 @@ +use std::{borrow::Cow, time::SystemTime}; + +use heed::{ + types::{Bytes, Str}, + BoxedError, BytesDecode, BytesEncode, Database, +}; + +use pubky_common::crypto::Hash; + +/// hash of the blob => bytes. +pub type BlobsTable = Database; + +pub const BLOBS_TABLE: &str = "blobs"; diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs new file mode 100644 index 0000000..5a1cc8e --- /dev/null +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -0,0 +1,66 @@ +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, time::SystemTime}; + +use heed::{ + types::{Bytes, Str}, + BoxedError, BytesDecode, BytesEncode, Database, +}; + +use pubky_common::crypto::Hash; + +/// full_path(pubky/*path) => Entry. +pub type EntriesTable = Database; + +pub const ENTRIES_TABLE: &str = "entries"; + +#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Entry { + /// Encoding version + version: usize, + /// Modified at + timestamp: u64, + content_hash: [u8; 32], + content_length: usize, + content_type: String, + // user_metadata: ? +} + +// TODO: get headers like Etag + +impl Entry { + pub fn new() -> Self { + Default::default() + } + + // === Setters === + + pub fn set_content_hash(&mut self, content_hash: Hash) -> &mut Self { + content_hash.as_bytes().clone_into(&mut self.content_hash); + self + } + + pub fn set_content_length(&mut self, content_length: usize) -> &mut Self { + self.content_length = content_length; + self + } + + pub fn set_content_type(&mut self, content_type: &str) -> &mut Self { + self.content_type = content_type.to_string(); + self + } + + // === Getters === + + pub fn content_hash(&self) -> &[u8; 32] { + &self.content_hash + } + + pub fn content_length(&self) -> usize { + self.content_length + } + + pub fn content_type(&self) -> &str { + &self.content_type + } +} diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 86120c2..6099858 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -1,4 +1,5 @@ use axum::{ + extract::DefaultBodyLimit, routing::{delete, get, post, put}, Router, }; @@ -18,8 +19,11 @@ pub fn create_app(state: AppState) -> Router { .route("/:pubky/session", get(auth::session)) .route("/:pubky/session", post(auth::signin)) .route("/:pubky/session", delete(auth::signout)) - .route("/:pubky/*key", get(drive::put)) + .route("/:pubky/*key", put(drive::put)) .layer(TraceLayer::new_for_http()) .layer(CookieManagerLayer::new()) + // TODO: revisit if we enable streaming big payloads + // TODO: maybe add to a separate router (drive router?). + .layer(DefaultBodyLimit::max(16 * 1024)) .with_state(state) } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index fceb6fe..c38aa38 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -2,7 +2,6 @@ use axum::{ extract::{Request, State}, http::{HeaderMap, StatusCode}, response::IntoResponse, - routing::get, Router, }; use axum_extra::{headers::UserAgent, TypedHeader}; @@ -103,7 +102,11 @@ pub async fn signin( state.verifier.verify(&body, public_key)?; let mut wtxn = state.db.env.write_txn()?; - let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; + let users: UsersTable = state + .db + .env + .open_database(&wtxn, Some(USERS_TABLE))? + .expect("Users table already created"); if let Some(existing) = users.get(&wtxn, public_key)? { users.put(&mut wtxn, public_key, &existing)?; diff --git a/pubky-homeserver/src/routes/drive.rs b/pubky-homeserver/src/routes/drive.rs index 3050250..12f8fb8 100644 --- a/pubky-homeserver/src/routes/drive.rs +++ b/pubky-homeserver/src/routes/drive.rs @@ -1,11 +1,63 @@ -use axum::response::IntoResponse; +use axum::{ + body::{Body, Bytes}, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + RequestExt, Router, +}; +use futures_util::stream::StreamExt; use tracing::debug; -use crate::extractors::Pubky; +use pubky_common::crypto::Hasher; -pub async fn put(pubky: Pubky) -> Result { - debug!(pubky=?pubky.public_key()); +use crate::{ + database::tables::blobs::{BlobsTable, BLOBS_TABLE}, + error::{Error, Result}, + extractors::Pubky, + server::AppState, +}; + +pub async fn put( + State(state): State, + pubky: Pubky, + // Path(key): Path, + mut body: Body, +) -> Result { + let mut stream = body.into_data_stream(); + + let (tx, rx) = flume::bounded::(1); + + // Offload the write transaction to a blocking task + let done = tokio::task::spawn_blocking(move || { + // TODO: this is a blocking operation, which is ok for small + // payloads (we have 16 kb limit for now) but later we need + // to stream this to filesystem, and keep track of any failed + // writes to GC these files later. + + let mut wtxn = state.db.env.write_txn().unwrap(); + let blobs: BlobsTable = state + .db + .env + .open_database(&wtxn, Some(BLOBS_TABLE)) + .unwrap() + .expect("Blobs table already created"); + + let hasher = Hasher::new(); + + while let Ok(chunk) = rx.recv() { + dbg!(chunk); + } + }); + + while let Some(next) = stream.next().await { + let chunk = next + .map_err(|err| Error::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())))?; + + tx.send(chunk); + } + + let _ = done.await; Ok("Pubky drive...".to_string()) } diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index f167d05..0a2f3ae 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -3,7 +3,7 @@ use std::{future::IntoFuture, net::SocketAddr}; use anyhow::{Error, Result}; use pubky_common::auth::AuthnVerifier; use tokio::{net::TcpListener, signal, task::JoinSet}; -use tracing::{info, warn}; +use tracing::{debug, info, warn}; use pkarr::{ mainline::dht::{DhtSettings, Testnet}, @@ -27,6 +27,8 @@ pub(crate) struct AppState { impl Homeserver { pub async fn start(config: Config) -> Result { + debug!(?config); + let public_key = config.keypair().public_key(); let db = DB::open(&config.storage()?)?; From 62cc13bca489e568882757ed851e06841ec3258a Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 23 Jul 2024 11:35:53 +0300 Subject: [PATCH 003/125] refactor(pubky): separate PubkyClient implementation into modules --- pubky/src/client.rs | 245 +------------------------------------- pubky/src/client/auth.rs | 122 +++++++++++++++++++ pubky/src/client/pkarr.rs | 179 ++++++++++++++++++++++++++++ pubky/src/lib.rs | 51 -------- 4 files changed, 305 insertions(+), 292 deletions(-) create mode 100644 pubky/src/client/auth.rs create mode 100644 pubky/src/client/pkarr.rs diff --git a/pubky/src/client.rs b/pubky/src/client.rs index e353e98..69f418c 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -1,18 +1,14 @@ +mod auth; +mod pkarr; + use std::{collections::HashMap, fmt::format, time::Duration}; -use pkarr::{ - dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, PublicKey, Settings, SignedPacket, -}; use ureq::{Agent, Response}; use url::Url; -use pubky_common::{auth::AuthnSignature, session::Session}; - use crate::error::{Error, Result}; -const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; +use pkarr::{DhtSettings, PkarrClient, Settings, Testnet}; #[derive(Debug, Clone)] pub struct PubkyClient { @@ -45,174 +41,8 @@ impl PubkyClient { // === Public Methods === - /// Signup to a homeserver and update Pkarr accordingly. - /// - /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key - /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" - pub fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (audience, mut url) = self.resolve_endpoint(homeserver)?; - - url.set_path(&format!("/{}", keypair.public_key())); - - self.request(HttpMethod::Put, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) - .map_err(Box::new)?; - - self.publish_pubky_homeserver(keypair, homeserver); - - Ok(()) - } - - /// Check the current sesison for a given Pubky in its homeserver. - pub fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; - - url.set_path(&format!("/{}/session", pubky)); - - let mut bytes = vec![]; - - let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new); - - if let Ok(reader) = result { - reader.into_reader().read_to_end(&mut bytes); - } else { - return Err(Error::NotSignedIn); - } - - Ok(Session::deserialize(&bytes)?) - } - - /// Signout from a homeserver. - pub fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; - - url.set_path(&format!("/{}/session", pubky)); - - self.request(HttpMethod::Delete, &url) - .call() - .map_err(Box::new)?; - - Ok(()) - } - - /// Signin to a homeserver. - pub fn signin(&self, keypair: &Keypair) -> Result<()> { - let pubky = keypair.public_key(); - - let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?; - - url.set_path(&format!("/{}/session", &pubky)); - - self.request(HttpMethod::Post, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) - .map_err(Box::new)?; - - Ok(()) - } - // === Private Methods === - /// Publish the SVCB record for `_pubky.`. - pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let mut packet = Packet::new_reply(0); - - if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let svcb = SVCB::new(0, host.try_into()?); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; - - self.pkarr.publish(&signed_packet)?; - - Ok(()) - } - - /// Resolve the homeserver for a pubky. - pub(crate) fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, Url)> { - let target = format!("_pubky.{}", pubky); - - self.resolve_endpoint(&target) - .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) - } - - /// Resolve a service's public_key and clearnet url from a Pubky domain - fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { - // TODO: cache the result of this function? - // TODO: use MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION - // TODO: move to common? - - let mut target = target.to_string(); - let mut homeserver_public_key = None; - let mut host = target.clone(); - - // PublicKey is very good at extracting the Pkarr TLD from a string. - while let Ok(public_key) = PublicKey::try_from(target.clone()) { - if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { - let mut prior = None; - - for answer in signed_packet.resource_records(&target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) - } - // TODO return random if priority is the same - } else { - prior = Some(svcb) - } - } - } - - if let Some(svcb) = prior { - homeserver_public_key = Some(public_key); - target = svcb.target.to_string(); - - if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { - if port.len() < 2 { - // TODO: debug! Error encoding port! - } - let port = u16::from_be_bytes([port[0], port[1]]); - - host = format!("{target}:{port}"); - } else { - host.clone_from(&target); - }; - - continue; - } - }; - - break; - } - - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; - - return Ok((homeserver, Url::parse(&url)?)); - } - - Err(Error::Generic("Could not resolve endpoint".to_string())) - } - fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request { self.agent.request_url(method.into(), url) } @@ -242,70 +72,3 @@ impl From for &str { } } } - -#[cfg(test)] -mod tests { - use super::*; - - use pkarr::{ - dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, Settings, SignedPacket, - }; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn resolve_homeserver() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - // Publish an intermediate controller of the homeserver - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - ..Default::default() - }, - ..Default::default() - }) - .unwrap() - .as_async(); - - let intermediate = Keypair::random(); - - let mut packet = Packet::new_reply(0); - - let server_tld = server.public_key().to_string(); - - let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); - - pkarr_client.publish(&signed_packet).await.unwrap(); - - tokio::task::spawn_blocking(move || { - let client = PubkyClient::test(&testnet); - - let pubky = Keypair::random(); - - client - .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); - - let (public_key, url) = client - .resolve_pubky_homeserver(&pubky.public_key()) - .unwrap(); - - assert_eq!(public_key, server.public_key()); - assert_eq!(url.host_str(), Some("localhost")); - assert_eq!(url.port(), Some(server.port())); - }) - .await - .expect("task failed") - } -} diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs new file mode 100644 index 0000000..25b679c --- /dev/null +++ b/pubky/src/client/auth.rs @@ -0,0 +1,122 @@ +use crate::PubkyClient; + +use pubky_common::{auth::AuthnSignature, session::Session}; + +use super::{Error, HttpMethod, Result}; +use pkarr::{Keypair, PublicKey}; + +impl PubkyClient { + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + pub fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { + let (audience, mut url) = self.resolve_endpoint(homeserver)?; + + url.set_path(&format!("/{}", keypair.public_key())); + + self.request(HttpMethod::Put, &url) + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) + .map_err(Box::new)?; + + self.publish_pubky_homeserver(keypair, homeserver); + + Ok(()) + } + + /// Check the current sesison for a given Pubky in its homeserver. + pub fn session(&self, pubky: &PublicKey) -> Result { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + + url.set_path(&format!("/{}/session", pubky)); + + let mut bytes = vec![]; + + let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new); + + if let Ok(reader) = result { + reader.into_reader().read_to_end(&mut bytes); + } else { + return Err(Error::NotSignedIn); + } + + Ok(Session::deserialize(&bytes)?) + } + + /// Signout from a homeserver. + pub fn signout(&self, pubky: &PublicKey) -> Result<()> { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + + url.set_path(&format!("/{}/session", pubky)); + + self.request(HttpMethod::Delete, &url) + .call() + .map_err(Box::new)?; + + Ok(()) + } + + /// Signin to a homeserver. + pub fn signin(&self, keypair: &Keypair) -> Result<()> { + let pubky = keypair.public_key(); + + let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?; + + url.set_path(&format!("/{}/session", &pubky)); + + self.request(HttpMethod::Post, &url) + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) + .map_err(Box::new)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_common::session::Session; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn basic_authn() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet).as_async(); + + let keypair = Keypair::random(); + + client + .signup(&keypair, &server.public_key().to_string()) + .await + .unwrap(); + + let session = client.session(&keypair.public_key()).await.unwrap(); + + assert_eq!(session, Session { ..session.clone() }); + + client.signout(&keypair.public_key()).await.unwrap(); + + { + let session = client.session(&keypair.public_key()).await; + + assert!(session.is_err()); + + match session { + Err(Error::NotSignedIn) => {} + _ => assert!(false, "expected NotSignedInt error"), + } + } + + client.signin(&keypair).await.unwrap(); + + { + let session = client.session(&keypair.public_key()).await.unwrap(); + + assert_eq!(session, Session { ..session.clone() }); + } + } +} diff --git a/pubky/src/client/pkarr.rs b/pubky/src/client/pkarr.rs new file mode 100644 index 0000000..c036527 --- /dev/null +++ b/pubky/src/client/pkarr.rs @@ -0,0 +1,179 @@ +pub use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, PublicKey, Settings, SignedPacket, +}; + +use super::{Error, PubkyClient, Result, Url}; + +const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; + +impl PubkyClient { + /// Publish the SVCB record for `_pubky.`. + pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr.publish(&signed_packet)?; + + Ok(()) + } + + /// Resolve the homeserver for a pubky. + pub(crate) fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, Url)> { + let target = format!("_pubky.{}", pubky); + + self.resolve_endpoint(&target) + .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) + } + + /// Resolve a service's public_key and clearnet url from a Pubky domain + pub(crate) fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + // TODO: cache the result of this function? + // TODO: use MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION + // TODO: move to common? + + let mut target = target.to_string(); + let mut homeserver_public_key = None; + let mut host = target.clone(); + + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { + let mut prior = None; + + for answer in signed_packet.resource_records(&target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } + } + } + + if let Some(svcb) = prior { + homeserver_public_key = Some(public_key); + target = svcb.target.to_string(); + + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + host = format!("{target}:{port}"); + } else { + host.clone_from(&target); + }; + + continue; + } + }; + + break; + } + + if let Some(homeserver) = homeserver_public_key { + let url = if host.starts_with("localhost") { + format!("http://{host}") + } else { + format!("https://{host}") + }; + + return Ok((homeserver, Url::parse(&url)?)); + } + + Err(Error::Generic("Could not resolve endpoint".to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, Settings, SignedPacket, + }; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn resolve_homeserver() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + // Publish an intermediate controller of the homeserver + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let intermediate = Keypair::random(); + + let mut packet = Packet::new_reply(0); + + let server_tld = server.public_key().to_string(); + + let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + tokio::task::spawn_blocking(move || { + let client = PubkyClient::test(&testnet); + + let pubky = Keypair::random(); + + client + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); + + let (public_key, url) = client + .resolve_pubky_homeserver(&pubky.public_key()) + .unwrap(); + + assert_eq!(public_key, server.public_key()); + assert_eq!(url.host_str(), Some("localhost")); + assert_eq!(url.port(), Some(server.port())); + }) + .await + .expect("task failed") + } +} diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index b05d067..7125ca1 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -6,54 +6,3 @@ mod error; pub use client::PubkyClient; pub use error::Error; - -#[cfg(test)] -mod tests { - use super::*; - - use super::error::Error; - - use pkarr::{mainline::Testnet, Keypair}; - use pubky_common::session::Session; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn basic_authn() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - let client = PubkyClient::test(&testnet).as_async(); - - let keypair = Keypair::random(); - - client - .signup(&keypair, &server.public_key().to_string()) - .await - .unwrap(); - - let session = client.session(&keypair.public_key()).await.unwrap(); - - assert_eq!(session, Session { ..session.clone() }); - - client.signout(&keypair.public_key()).await.unwrap(); - - { - let session = client.session(&keypair.public_key()).await; - - assert!(session.is_err()); - - match session { - Err(Error::NotSignedIn) => {} - _ => assert!(false, "expected NotSignedInt error"), - } - } - - client.signin(&keypair).await.unwrap(); - - { - let session = client.session(&keypair.public_key()).await.unwrap(); - - assert_eq!(session, Session { ..session.clone() }); - } - } -} From 8cf18a3c0c1566dbdc4ac0e3f15b4789eed02d56 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 23 Jul 2024 19:04:02 +0300 Subject: [PATCH 004/125] feat(pubky): add get() --- pubky-homeserver/src/database/tables/blobs.rs | 4 +- .../src/database/tables/entries.rs | 23 ++- pubky-homeserver/src/error.rs | 40 +++-- pubky-homeserver/src/extractors.rs | 29 ++++ pubky-homeserver/src/routes.rs | 5 +- pubky-homeserver/src/routes/drive.rs | 63 -------- pubky-homeserver/src/routes/public.rs | 149 ++++++++++++++++++ pubky/Cargo.toml | 1 + pubky/src/client.rs | 1 + pubky/src/client/auth.rs | 20 ++- pubky/src/client/public.rs | 115 ++++++++++++++ pubky/src/client_async.rs | 29 ++++ pubky/src/error.rs | 6 + 13 files changed, 395 insertions(+), 90 deletions(-) delete mode 100644 pubky-homeserver/src/routes/drive.rs create mode 100644 pubky-homeserver/src/routes/public.rs create mode 100644 pubky/src/client/public.rs diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs index 9cf1da1..0148d6f 100644 --- a/pubky-homeserver/src/database/tables/blobs.rs +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -5,9 +5,7 @@ use heed::{ BoxedError, BytesDecode, BytesEncode, Database, }; -use pubky_common::crypto::Hash; - /// hash of the blob => bytes. -pub type BlobsTable = Database; +pub type BlobsTable = Database; pub const BLOBS_TABLE: &str = "blobs"; diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 5a1cc8e..1d2028e 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -7,10 +7,10 @@ use heed::{ BoxedError, BytesDecode, BytesEncode, Database, }; -use pubky_common::crypto::Hash; +use pubky_common::{crypto::Hash, timestamp::Timestamp}; /// full_path(pubky/*path) => Entry. -pub type EntriesTable = Database; +pub type EntriesTable = Database; pub const ENTRIES_TABLE: &str = "entries"; @@ -30,7 +30,10 @@ pub struct Entry { impl Entry { pub fn new() -> Self { - Default::default() + Self { + timestamp: Timestamp::now().into_inner(), + ..Default::default() + } } // === Setters === @@ -63,4 +66,18 @@ impl Entry { pub fn content_type(&self) -> &str { &self.content_type } + + // === Public Method === + + pub fn serialize(&self) -> Vec { + to_allocvec(self).expect("Session::serialize") + } + + pub fn deserialize(bytes: &[u8]) -> core::result::Result { + if bytes[0] > 0 { + panic!("Unknown Entry version"); + } + + Ok(from_bytes(bytes)?) + } } diff --git a/pubky-homeserver/src/error.rs b/pubky-homeserver/src/error.rs index 46d37d6..57081ab 100644 --- a/pubky-homeserver/src/error.rs +++ b/pubky-homeserver/src/error.rs @@ -54,25 +54,49 @@ impl IntoResponse for Error { impl From for Error { fn from(error: QueryRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) + Self::new(StatusCode::BAD_REQUEST, error.into()) } } impl From for Error { fn from(error: ExtensionRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) + Self::new(StatusCode::BAD_REQUEST, error.into()) } } impl From for Error { fn from(error: PathRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) + Self::new(StatusCode::BAD_REQUEST, error.into()) } } impl From for Error { fn from(error: std::io::Error) -> Self { - Self::new(StatusCode::INTERNAL_SERVER_ERROR, Some(error)) + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: heed::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: anyhow::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: postcard::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) + } +} + +impl From for Error { + fn from(error: axum::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) } } @@ -89,11 +113,3 @@ impl From for Error { Self::new(StatusCode::BAD_REQUEST, Some(error)) } } - -impl From for Error { - fn from(error: heed::Error) -> Self { - debug!(?error); - - Self::with_status(StatusCode::INTERNAL_SERVER_ERROR) - } -} diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs index be65f13..e7192db 100644 --- a/pubky-homeserver/src/extractors.rs +++ b/pubky-homeserver/src/extractors.rs @@ -45,3 +45,32 @@ where Ok(Pubky(public_key)) } } + +pub struct EntryPath(pub(crate) String); + +impl EntryPath { + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[async_trait] +impl FromRequestParts for EntryPath +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let params: Path> = + parts.extract().await.map_err(IntoResponse::into_response)?; + + // TODO: enforce path limits like no trailing '/' + + let path = params + .get("path") + .ok_or_else(|| (StatusCode::NOT_FOUND, "entry path missing").into_response())?; + + Ok(EntryPath(path.to_string())) + } +} diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 6099858..3b872c1 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -9,7 +9,7 @@ use tower_http::trace::TraceLayer; use crate::server::AppState; mod auth; -mod drive; +mod public; mod root; pub fn create_app(state: AppState) -> Router { @@ -19,7 +19,8 @@ pub fn create_app(state: AppState) -> Router { .route("/:pubky/session", get(auth::session)) .route("/:pubky/session", post(auth::signin)) .route("/:pubky/session", delete(auth::signout)) - .route("/:pubky/*key", put(drive::put)) + .route("/:pubky/*path", put(public::put)) + .route("/:pubky/*path", get(public::get)) .layer(TraceLayer::new_for_http()) .layer(CookieManagerLayer::new()) // TODO: revisit if we enable streaming big payloads diff --git a/pubky-homeserver/src/routes/drive.rs b/pubky-homeserver/src/routes/drive.rs deleted file mode 100644 index 12f8fb8..0000000 --- a/pubky-homeserver/src/routes/drive.rs +++ /dev/null @@ -1,63 +0,0 @@ -use axum::{ - body::{Body, Bytes}, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, - RequestExt, Router, -}; -use futures_util::stream::StreamExt; - -use tracing::debug; - -use pubky_common::crypto::Hasher; - -use crate::{ - database::tables::blobs::{BlobsTable, BLOBS_TABLE}, - error::{Error, Result}, - extractors::Pubky, - server::AppState, -}; - -pub async fn put( - State(state): State, - pubky: Pubky, - // Path(key): Path, - mut body: Body, -) -> Result { - let mut stream = body.into_data_stream(); - - let (tx, rx) = flume::bounded::(1); - - // Offload the write transaction to a blocking task - let done = tokio::task::spawn_blocking(move || { - // TODO: this is a blocking operation, which is ok for small - // payloads (we have 16 kb limit for now) but later we need - // to stream this to filesystem, and keep track of any failed - // writes to GC these files later. - - let mut wtxn = state.db.env.write_txn().unwrap(); - let blobs: BlobsTable = state - .db - .env - .open_database(&wtxn, Some(BLOBS_TABLE)) - .unwrap() - .expect("Blobs table already created"); - - let hasher = Hasher::new(); - - while let Ok(chunk) = rx.recv() { - dbg!(chunk); - } - }); - - while let Some(next) = stream.next().await { - let chunk = next - .map_err(|err| Error::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())))?; - - tx.send(chunk); - } - - let _ = done.await; - - Ok("Pubky drive...".to_string()) -} diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs new file mode 100644 index 0000000..38c2741 --- /dev/null +++ b/pubky-homeserver/src/routes/public.rs @@ -0,0 +1,149 @@ +use axum::{ + body::{Body, Bytes}, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + RequestExt, Router, +}; +use axum_extra::body::AsyncReadBody; +use futures_util::stream::StreamExt; + +use tracing::debug; + +use pubky_common::crypto::Hasher; + +use crate::{ + database::tables::{ + blobs::{BlobsTable, BLOBS_TABLE}, + entries::{EntriesTable, Entry, ENTRIES_TABLE}, + }, + error::{Error, Result}, + extractors::{EntryPath, Pubky}, + server::AppState, +}; + +pub async fn put( + State(state): State, + pubky: Pubky, + path: EntryPath, + mut body: Body, +) -> Result { + // TODO: return an error if path does not start with '/pub/' + + let mut stream = body.into_data_stream(); + + let (tx, rx) = flume::bounded::(1); + + // TODO: refactor Database to clean up this scope. + let done = tokio::task::spawn_blocking(move || -> Result<()> { + // TODO: this is a blocking operation, which is ok for small + // payloads (we have 16 kb limit for now) but later we need + // to stream this to filesystem, and keep track of any failed + // writes to GC these files later. + + let public_key = pubky.public_key(); + + // TODO: Authorize + + let mut wtxn = state.db.env.write_txn()?; + let blobs: BlobsTable = state + .db + .env + .open_database(&wtxn, Some(BLOBS_TABLE))? + .expect("Blobs table already created"); + + let entries: EntriesTable = state + .db + .env + .open_database(&wtxn, Some(ENTRIES_TABLE))? + .expect("Entries table already created"); + + let mut hasher = Hasher::new(); + let mut bytes = vec![]; + let mut length = 0; + + while let Ok(chunk) = rx.recv() { + hasher.update(&chunk); + bytes.extend_from_slice(&chunk); + length += chunk.len(); + } + + let hash = hasher.finalize(); + + blobs.put(&mut wtxn, hash.as_bytes(), &bytes)?; + + let mut entry = Entry::new(); + + entry.set_content_hash(hash); + entry.set_content_length(length); + + let mut key = vec![]; + key.extend_from_slice(public_key.as_bytes()); + key.extend_from_slice(path.as_bytes()); + + entries.put(&mut wtxn, &key, &entry.serialize()); + + Ok(()) + }); + + while let Some(next) = stream.next().await { + let chunk = next?; + + tx.send(chunk); + } + + drop(tx); + done.await.expect("join error")?; + + // TODO: return relevant headers, like Etag? + + Ok(()) +} + +pub async fn get( + State(state): State, + pubky: Pubky, + path: EntryPath, +) -> Result { + // TODO: check the path, return an error if doesn't start with `/pub/` + + // TODO: Enable streaming + + let public_key = pubky.public_key(); + + let mut rtxn = state.db.env.read_txn()?; + + let entries: EntriesTable = state + .db + .env + .open_database(&rtxn, Some(ENTRIES_TABLE))? + .expect("Entries table already created"); + + let blobs: BlobsTable = state + .db + .env + .open_database(&rtxn, Some(BLOBS_TABLE))? + .expect("Blobs table already created"); + + let mut count = 0; + + for x in entries.iter(&rtxn)? { + count += 1 + } + + return Err(Error::new(StatusCode::NOT_FOUND, count.to_string().into())); + + let mut key = vec![]; + key.extend_from_slice(public_key.as_bytes()); + key.extend_from_slice(path.as_bytes()); + + if let Some(bytes) = entries.get(&rtxn, &key)? { + let entry = Entry::deserialize(bytes)?; + + if let Some(blob) = blobs.get(&rtxn, entry.content_hash())? { + return Ok(blob.to_vec()); + }; + }; + + Err(Error::new(StatusCode::NOT_FOUND, path.0.into())) +} diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index e10789a..c40cd9c 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -11,6 +11,7 @@ ureq = { version = "2.10.0", features = ["cookies"] } thiserror = "1.0.62" url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } +bytes = "1.6.1" [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 69f418c..95742df 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -1,5 +1,6 @@ mod auth; mod pkarr; +mod public; use std::{collections::HashMap, fmt::format, time::Duration}; diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs index 25b679c..8445640 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/client/auth.rs @@ -16,8 +16,7 @@ impl PubkyClient { url.set_path(&format!("/{}", keypair.public_key())); self.request(HttpMethod::Put, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) - .map_err(Box::new)?; + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())?; self.publish_pubky_homeserver(keypair, homeserver); @@ -25,6 +24,9 @@ impl PubkyClient { } /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns an [Error::NotSignedIn] if so, or [ureq::Error] if + /// the response has any other `>=400` status code. pub fn session(&self, pubky: &PublicKey) -> Result { let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; @@ -34,11 +36,15 @@ impl PubkyClient { let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new); - if let Ok(reader) = result { - reader.into_reader().read_to_end(&mut bytes); - } else { - return Err(Error::NotSignedIn); - } + let reader = self.request(HttpMethod::Get, &url).call().map_err(|err| { + match err { + ureq::Error::Status(404, _) => Error::NotSignedIn, + // TODO: handle other types of errors + _ => err.into(), + } + })?; + + reader.into_reader().read_to_end(&mut bytes); Ok(Session::deserialize(&bytes)?) } diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs new file mode 100644 index 0000000..1600c92 --- /dev/null +++ b/pubky/src/client/public.rs @@ -0,0 +1,115 @@ +use bytes::Bytes; + +use pkarr::PublicKey; + +use crate::PubkyClient; + +use super::Result; + +impl PubkyClient { + pub fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky)?; + + url.set_path(&format!("/{pubky}/{path}")); + + self.request(super::HttpMethod::Put, &url) + .send_bytes(content)?; + + Ok(()) + } + + pub fn get(&self, pubky: &PublicKey, path: &str) -> Result { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky)?; + + url.set_path(&format!("/{pubky}/{path}")); + + let result = self.request(super::HttpMethod::Get, &url).call(); + + if let Err(error) = result { + dbg!(&error); + + return Err(error)?; + } + + let response = result.unwrap(); + + let len = response + .header("Content-Length") + .and_then(|s| s.parse::().ok()) + // TODO: return an error in case content-length header is missing + .unwrap_or(0); + + // TODO: bail on too large files. + + let mut bytes = Vec::with_capacity(len as usize); + + response.into_reader().read_exact(&mut bytes); + + Ok(bytes.into()) + } +} + +fn normalize_path(path: &str) -> String { + let mut path = path.to_string(); + + if path.starts_with('/') { + path = path[1..].to_string() + } + + // TODO: should we return error instead? + if path.ends_with('/') { + path = path[..path.len()].to_string() + } + + path +} + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use crate::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_common::session::Session; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn put_get() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet).as_async(); + + let keypair = Keypair::random(); + + client + .signup(&keypair, &server.public_key().to_string()) + .await + .unwrap(); + + let response = client + .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) + .await; + + if let Err(Error::Ureq(ureqerror)) = response { + if let Some(r) = ureqerror.into_response() { + dbg!(r.into_string()); + } + } + + let response = client.get(&keypair.public_key(), "/pub/foo.txt").await; + + if let Err(Error::Ureq(ureqerror)) = response { + if let Some(r) = ureqerror.into_response() { + dbg!(r.into_string()); + } + } + + // dbg!(response); + } +} diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index de9012c..2fb7bd5 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -1,5 +1,7 @@ use std::thread; +use bytes::Bytes; + use pkarr::{Keypair, PublicKey}; use pubky_common::session::Session; @@ -62,4 +64,31 @@ impl PubkyClientAsync { receiver.recv_async().await? } + + /// Async version of [PubkyClient::put] + pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let pubky = pubky.clone(); + let path = path.to_string(); + let content = content.to_vec(); + + thread::spawn(move || sender.send(client.put(&pubky, &path, &content))); + + receiver.recv_async().await? + } + + /// Async version of [PubkyClient::get] + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let pubky = pubky.clone(); + let path = path.to_string(); + + thread::spawn(move || sender.send(client.get(&pubky, &path))); + + receiver.recv_async().await? + } } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 026382e..acbad4b 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -34,3 +34,9 @@ pub enum Error { #[error(transparent)] Session(#[from] pubky_common::session::Error), } + +impl From for Error { + fn from(error: ureq::Error) -> Self { + Error::Ureq(Box::new(error)) + } +} From cc97744f25c8a8e479f08b9231905f5cf6e4ddda Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 23 Jul 2024 21:15:41 +0300 Subject: [PATCH 005/125] feat(pubky): get successful --- Cargo.lock | 1 + pubky-homeserver/src/database.rs | 113 +++++++++++++++++- pubky-homeserver/src/database/migrations.rs | 10 +- .../src/database/migrations/m0.rs | 2 +- pubky-homeserver/src/database/tables.rs | 26 ++++ .../src/database/tables/entries.rs | 2 +- pubky-homeserver/src/extractors.rs | 4 + pubky-homeserver/src/routes/public.rs | 80 ++----------- pubky/src/client/public.rs | 29 ++--- 9 files changed, 167 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74ef877..613c9c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1250,6 +1250,7 @@ dependencies = [ name = "pubky" version = "0.1.0" dependencies = [ + "bytes", "flume", "pkarr", "pubky-common", diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 0eb3200..5bbe6c2 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -1,16 +1,23 @@ use std::fs; use std::path::Path; +use bytes::Bytes; use heed::{types::Str, Database, Env, EnvOpenOptions, RwTxn}; mod migrations; pub mod tables; -use migrations::TABLES_COUNT; +use pubky_common::crypto::Hasher; + +use tables::{entries::Entry, Tables, TABLES_COUNT}; + +use pkarr::PublicKey; +use tables::blobs::{BlobsTable, BLOBS_TABLE}; #[derive(Debug, Clone)] pub struct DB { pub(crate) env: Env, + pub(crate) tables: Tables, } impl DB { @@ -19,10 +26,110 @@ impl DB { let env = unsafe { EnvOpenOptions::new().max_dbs(TABLES_COUNT).open(storage) }?; - migrations::run(&env); + let tables = migrations::run(&env)?; - let db = DB { env }; + let db = DB { env, tables }; Ok(db) } + + pub fn put_entry( + &mut self, + public_key: &PublicKey, + path: &str, + rx: flume::Receiver, + ) -> anyhow::Result<()> { + let mut wtxn = self.env.write_txn()?; + + let mut hasher = Hasher::new(); + let mut bytes = vec![]; + let mut length = 0; + + while let Ok(chunk) = rx.recv() { + hasher.update(&chunk); + bytes.extend_from_slice(&chunk); + length += chunk.len(); + } + + let hash = hasher.finalize(); + + self.tables.blobs.put(&mut wtxn, hash.as_bytes(), &bytes)?; + + let mut entry = Entry::new(); + + entry.set_content_hash(hash); + entry.set_content_length(length); + + let mut key = vec![]; + key.extend_from_slice(public_key.as_bytes()); + key.extend_from_slice(path.as_bytes()); + + self.tables.entries.put(&mut wtxn, &key, &entry.serialize()); + + wtxn.commit()?; + + Ok(()) + } + + pub fn get_blob( + &mut self, + public_key: &PublicKey, + path: &str, + ) -> anyhow::Result> { + let mut rtxn = self.env.read_txn()?; + + let mut key = vec![]; + key.extend_from_slice(public_key.as_bytes()); + key.extend_from_slice(path.as_bytes()); + + if let Some(bytes) = self.tables.entries.get(&rtxn, &key)? { + let entry = Entry::deserialize(bytes)?; + + if let Some(blob) = self.tables.blobs.get(&rtxn, entry.content_hash())? { + return Ok(Some(Bytes::from(blob.to_vec()))); + }; + }; + + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use pkarr::Keypair; + use pubky_common::timestamp::Timestamp; + + use crate::config::Config; + + use super::{Bytes, DB}; + + #[tokio::test] + async fn entries() { + let storage = std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join("pubky"); + + let mut db = DB::open(&storage).unwrap(); + + let keypair = Keypair::random(); + let path = "/pub/foo.txt"; + + let (tx, rx) = flume::bounded::(0); + + let mut cloned = db.clone(); + let cloned_keypair = keypair.clone(); + + let done = tokio::task::spawn_blocking(move || { + cloned.put_entry(&cloned_keypair.public_key(), path, rx); + }); + + tx.send(vec![1, 2, 3, 4, 5].into()); + drop(tx); + + done.await; + + let blob = db.get_blob(&keypair.public_key(), path).unwrap().unwrap(); + + assert_eq!(blob, Bytes::from(vec![1, 2, 3, 4, 5])); + } } diff --git a/pubky-homeserver/src/database/migrations.rs b/pubky-homeserver/src/database/migrations.rs index dbead07..32f2909 100644 --- a/pubky-homeserver/src/database/migrations.rs +++ b/pubky-homeserver/src/database/migrations.rs @@ -2,16 +2,16 @@ use heed::{types::Str, Database, Env, RwTxn}; mod m0; -use super::tables; +use super::tables::Tables; -pub const TABLES_COUNT: u32 = 4; - -pub fn run(env: &Env) -> anyhow::Result<()> { +pub fn run(env: &Env) -> anyhow::Result { let mut wtxn = env.write_txn()?; m0::run(env, &mut wtxn); + let tables = Tables::new(env, &mut wtxn)?; + wtxn.commit()?; - Ok(()) + Ok(tables) } diff --git a/pubky-homeserver/src/database/migrations/m0.rs b/pubky-homeserver/src/database/migrations/m0.rs index 74d89c4..d7dac79 100644 --- a/pubky-homeserver/src/database/migrations/m0.rs +++ b/pubky-homeserver/src/database/migrations/m0.rs @@ -1,6 +1,6 @@ use heed::{types::Str, Database, Env, RwTxn}; -use super::tables::{blobs, entries, sessions, users}; +use crate::database::tables::{blobs, entries, sessions, users}; pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { let _: users::UsersTable = env.create_database(wtxn, Some(users::USERS_TABLE))?; diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index 4f0c1c5..a019fbe 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -2,3 +2,29 @@ pub mod blobs; pub mod entries; pub mod sessions; pub mod users; + +use heed::{Env, RwTxn}; + +use blobs::{BlobsTable, BLOBS_TABLE}; +use entries::{EntriesTable, ENTRIES_TABLE}; + +pub const TABLES_COUNT: u32 = 4; + +#[derive(Debug, Clone)] +pub struct Tables { + pub blobs: BlobsTable, + pub entries: EntriesTable, +} + +impl Tables { + pub fn new(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result { + Ok(Self { + blobs: env + .open_database(wtxn, Some(BLOBS_TABLE))? + .expect("Blobs table already created"), + entries: env + .open_database(wtxn, Some(ENTRIES_TABLE))? + .expect("Entries table already created"), + }) + } +} diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 1d2028e..7c9e2e3 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -78,6 +78,6 @@ impl Entry { panic!("Unknown Entry version"); } - Ok(from_bytes(bytes)?) + from_bytes(bytes) } } diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs index e7192db..b89911c 100644 --- a/pubky-homeserver/src/extractors.rs +++ b/pubky-homeserver/src/extractors.rs @@ -49,6 +49,10 @@ where pub struct EntryPath(pub(crate) String); impl EntryPath { + pub fn as_str(&self) -> &str { + self.0.as_str() + } + pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 38c2741..cb07cee 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -23,7 +23,7 @@ use crate::{ }; pub async fn put( - State(state): State, + State(mut state): State, pubky: Pubky, path: EntryPath, mut body: Body, @@ -45,43 +45,7 @@ pub async fn put( // TODO: Authorize - let mut wtxn = state.db.env.write_txn()?; - let blobs: BlobsTable = state - .db - .env - .open_database(&wtxn, Some(BLOBS_TABLE))? - .expect("Blobs table already created"); - - let entries: EntriesTable = state - .db - .env - .open_database(&wtxn, Some(ENTRIES_TABLE))? - .expect("Entries table already created"); - - let mut hasher = Hasher::new(); - let mut bytes = vec![]; - let mut length = 0; - - while let Ok(chunk) = rx.recv() { - hasher.update(&chunk); - bytes.extend_from_slice(&chunk); - length += chunk.len(); - } - - let hash = hasher.finalize(); - - blobs.put(&mut wtxn, hash.as_bytes(), &bytes)?; - - let mut entry = Entry::new(); - - entry.set_content_hash(hash); - entry.set_content_length(length); - - let mut key = vec![]; - key.extend_from_slice(public_key.as_bytes()); - key.extend_from_slice(path.as_bytes()); - - entries.put(&mut wtxn, &key, &entry.serialize()); + state.db.put_entry(public_key, path.as_str(), rx); Ok(()) }); @@ -101,7 +65,7 @@ pub async fn put( } pub async fn get( - State(state): State, + State(mut state): State, pubky: Pubky, path: EntryPath, ) -> Result { @@ -111,39 +75,9 @@ pub async fn get( let public_key = pubky.public_key(); - let mut rtxn = state.db.env.read_txn()?; - - let entries: EntriesTable = state - .db - .env - .open_database(&rtxn, Some(ENTRIES_TABLE))? - .expect("Entries table already created"); - - let blobs: BlobsTable = state - .db - .env - .open_database(&rtxn, Some(BLOBS_TABLE))? - .expect("Blobs table already created"); - - let mut count = 0; - - for x in entries.iter(&rtxn)? { - count += 1 + match state.db.get_blob(public_key, path.as_str()) { + Err(error) => Err(error)?, + Ok(Some(bytes)) => Ok(bytes), + Ok(None) => Err(Error::with_status(StatusCode::NOT_FOUND)), } - - return Err(Error::new(StatusCode::NOT_FOUND, count.to_string().into())); - - let mut key = vec![]; - key.extend_from_slice(public_key.as_bytes()); - key.extend_from_slice(path.as_bytes()); - - if let Some(bytes) = entries.get(&rtxn, &key)? { - let entry = Entry::deserialize(bytes)?; - - if let Some(blob) = blobs.get(&rtxn, entry.content_hash())? { - return Ok(blob.to_vec()); - }; - }; - - Err(Error::new(StatusCode::NOT_FOUND, path.0.into())) } diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs index 1600c92..7599e8f 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/client/public.rs @@ -27,15 +27,7 @@ impl PubkyClient { url.set_path(&format!("/{pubky}/{path}")); - let result = self.request(super::HttpMethod::Get, &url).call(); - - if let Err(error) = result { - dbg!(&error); - - return Err(error)?; - } - - let response = result.unwrap(); + let response = self.request(super::HttpMethod::Get, &url).call()?; let len = response .header("Content-Length") @@ -45,7 +37,7 @@ impl PubkyClient { // TODO: bail on too large files. - let mut bytes = Vec::with_capacity(len as usize); + let mut bytes = vec![0; len as usize]; response.into_reader().read_exact(&mut bytes); @@ -102,14 +94,17 @@ mod tests { } } - let response = client.get(&keypair.public_key(), "/pub/foo.txt").await; + let response = client + .get(&keypair.public_key(), "/pub/foo.txt") + .await + .unwrap(); - if let Err(Error::Ureq(ureqerror)) = response { - if let Some(r) = ureqerror.into_response() { - dbg!(r.into_string()); - } - } + // if let Err(Error::Ureq(ureqerror)) = response { + // if let Some(r) = ureqerror.into_response() { + // dbg!(r.into_string()); + // } + // } - // dbg!(response); + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) } } From 979882b44326183f8bbb4cf5bfedccb189c6741e Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 24 Jul 2024 18:02:41 +0300 Subject: [PATCH 006/125] feat(js): add common modules --- js/pubky/.gitignore | 6 + js/pubky/package.json | 51 ++++++++ js/pubky/src/common/auth.js | 194 ++++++++++++++++++++++++++++++ js/pubky/src/common/crypto.js | 131 ++++++++++++++++++++ js/pubky/src/common/index.js | 8 ++ js/pubky/src/common/namespaces.js | 1 + js/pubky/src/common/timestamp.js | 53 ++++++++ js/pubky/tsconfig.json | 32 +++++ 8 files changed, 476 insertions(+) create mode 100644 js/pubky/.gitignore create mode 100644 js/pubky/package.json create mode 100644 js/pubky/src/common/auth.js create mode 100644 js/pubky/src/common/crypto.js create mode 100644 js/pubky/src/common/index.js create mode 100644 js/pubky/src/common/namespaces.js create mode 100644 js/pubky/src/common/timestamp.js create mode 100644 js/pubky/tsconfig.json diff --git a/js/pubky/.gitignore b/js/pubky/.gitignore new file mode 100644 index 0000000..6da14b1 --- /dev/null +++ b/js/pubky/.gitignore @@ -0,0 +1,6 @@ +coverage +node_modules +types +.storage +.env +package-lock.json diff --git a/js/pubky/package.json b/js/pubky/package.json new file mode 100644 index 0000000..08a46cf --- /dev/null +++ b/js/pubky/package.json @@ -0,0 +1,51 @@ +{ + "name": "@synonymdev/pubky", + "version": "0.1.0", + "description": "Pubky client library", + "type": "module", + "main": "src/index.js", + "types": "types/src/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/slashtags/skunk-works.git" + }, + "scripts": { + "build": "tsc", + "clean": "rm -rf types", + "lint": "standard --fix", + "test": "brittle test/*.js -cov", + "depcheck": "npx depcheck --ignore-dirs=test", + "fullcheck": "npm run lint && npm run clean && npm run build && npm run test && npm run depcheck", + "prepublishOnly": "npm run fullcheck" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pubky/pubky/issues" + }, + "homepage": "https://github.com/pubky/pubky/tree/master/js/pubky/#readme", + "files": [ + "src", + "types", + "!**/*.tsbuildinfo" + ], + "dependencies": { + "blake3-wasm": "^3.0.0", + "crockford-base32": "^2.0.0", + "eventsource": "^2.0.2", + "hash-wasm": "^4.11.0", + "node-fetch-cache": "^4.1.2", + "pkarr": "^1.4.1", + "z32": "^1.1.0" + }, + "browser": { + "./src/lib/fetch.js": "./src/lib/fetch-browser.js" + }, + "devDependencies": { + "standard": "^17.1.0", + "typescript": "^5.5.4" + }, + "overrides": { + "blake3-wasm@2.1.7": "^3.0.0", + "@c4312/blake3-internal": "^3.0.0" + } +} diff --git a/js/pubky/src/common/auth.js b/js/pubky/src/common/auth.js new file mode 100644 index 0000000..f48d387 --- /dev/null +++ b/js/pubky/src/common/auth.js @@ -0,0 +1,194 @@ +import { Timestamp } from './timestamp.js' +import * as namespaces from './namespaces.js' +import * as crypto from './crypto.js' + +// 30 seconds +const TIME_INTERVAL = 30 * 1000000 + +export class AuthnSignature { + /** + * @param {number} time + * @param {import ('./crypto.js').KeyPair} signer + * @param {import ('./crypto.js').PublicKey} audience + * @param {Buffer} [token] + */ + constructor (time, signer, audience, token = crypto.randomBytes()) { + const timeStep = Math.floor(time / TIME_INTERVAL) + + const tokenHash = crypto.hash(token) + + const timeStepBytes = Buffer.allocUnsafe(8) + timeStepBytes.writeBigUint64BE(BigInt(timeStep)) + + const signature = signer.sign(signable( + signer.publicKey().bytes, + audience.bytes, + timeStepBytes, + tokenHash + )) + + this.bytes = Buffer.concat([ + signature, + tokenHash + ]) + } + + /** + * @param {import ('./crypto.js').KeyPair} signer + * @param {import ('./crypto.js').PublicKey} audience + * @param {Buffer} [token] + */ + static sign (signer, audience, token = crypto.randomBytes()) { + const time = Timestamp.now().microseconds + + return new AuthnSignature(time, signer, audience, token) + } + + asBytes () { + return new Uint8Array(this.bytes) + } +} + +export class AuthnVerifier { + #audience + /** @type {Array} */ + #seen + + /** + * @param {crypto.PublicKey} audience + */ + constructor (audience) { + this.#audience = audience + + this.#seen = [] + } + + #gc () { + const threshold = Timestamp.now().microseconds + const threshouldStep = Math.floor(threshold / TIME_INTERVAL) - 2 + + const thresholdBytes = Buffer.allocUnsafe(8) + thresholdBytes.writeBigUint64BE(BigInt(threshouldStep)) + + let count = 0 + + for (let i = 0; i < this.#seen.length; i++) { + if (this.#seen[i].subarray(0, 8).compare(thresholdBytes) > 0) { + break + } + count = i + } + + this.#seen.splice(0, count) + } + + /** + * @param {Buffer} bytes + * @param {crypto.PublicKey} signer + * + * @returns {true | Error} + */ + verify (bytes, signer) { + this.#gc() + + if (bytes.length !== 96) { + throw new Error(`InvalidLength: ${bytes.length}`) + } + + const signature = bytes.subarray(0, 64) + const tokenHash = bytes.subarray(64) + + const now = Timestamp.now().microseconds + const past = now - TIME_INTERVAL + const future = now + TIME_INTERVAL + + let result = verifyAt.call(this, now) + + if (!(result instanceof Error)) { + return result + } else if (result.toString() === 'Error: AuthnSignature already used') { + return result + } + + result = verifyAt.call(this, past) + + if (!(result instanceof Error)) { + return result + } else if (result.toString() === 'Error: AuthnSignature already used') { + return result + } + + return verifyAt.call(this, future) + + /** + * @param {number} time + */ + function verifyAt (time) { + const timeStep = Math.floor(time / TIME_INTERVAL) + + const timeStepBytes = Buffer.allocUnsafe(8) + timeStepBytes.writeBigUint64BE(BigInt(timeStep)) + + const result = signer.verify(signature, signable(signer.bytes, this.#audience.bytes, timeStepBytes, tokenHash)) + + const candidate = Buffer.concat([ + timeStepBytes, + tokenHash + ]) + + if (!(result instanceof Error)) { + const index = binarySearch(this.#seen, timeStepBytes) + + if (this.#seen[index]?.equals(candidate)) { + return new Error('AuthnSignature already used') + } + + this.#seen.splice(~index, 0, candidate) + + return + } + + return result + } + } +} + +/** + * @param {Array} arr + */ +function binarySearch (arr, element) { + let left = 0 + let right = arr.length - 1 + + while (left <= right) { + const mid = Math.floor((left + right) / 2) + + const comparison = arr[mid].subarray(0, 8).compare(element.subarray(0, 8)) + + if (comparison === 0) { + return mid + } else if (comparison < 0) { + left = mid + 1 + } else { + right = mid - 1 + } + } + + return left // Element not found, return the index where it should be inserted +} + +/** + * @param {Buffer} signer + * @param {Buffer} audience + * @param {Buffer} timeStepBytes + * @param {Buffer} tokenHash + */ +function signable (signer, audience, timeStepBytes, tokenHash) { + return Buffer.concat([ + namespaces.PUBKY_AUTHN, + timeStepBytes, + signer, + audience, + tokenHash + ]) +} diff --git a/js/pubky/src/common/crypto.js b/js/pubky/src/common/crypto.js new file mode 100644 index 0000000..c1c81b0 --- /dev/null +++ b/js/pubky/src/common/crypto.js @@ -0,0 +1,131 @@ +//! Crypeo functions + +import sodium from 'sodium-universal' +import z32 from 'z32' + +// Blake3 + +/** @type {import('blake3-wasm')} */ +let loadedBlake3 + +const loadBlake3 = async () => { + if (loadedBlake3) return loadedBlake3 + // @ts-ignore + loadedBlake3 = await import('blake3-wasm').then(b3 => b3.load().then(() => b3)) + + return loadedBlake3 +} + +loadBlake3() + +/** + * It will return null if blake3 is not loaded yet! + * + * @param {Buffer} message + * + * @returns {Buffer | null} + */ +export const hash = (message) => { + return loadedBlake3?.createHash().update(message).digest() +} + +// Random +export const randomBytes = (n = 32) => { + const buf = Buffer.alloc(n) + sodium.randombytes_buf(buf) + return buf +} + +/// Keypairs + +/** + * @param {Buffer} buf + */ +export const zeroize = (buf) => { + buf.fill(0) +} + +export class KeyPair { + #publicKey + #secretKey + + /** + * @param {Buffer} seed + */ + constructor (seed) { + this.#publicKey = Buffer.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES) + this.#secretKey = Buffer.allocUnsafe(sodium.crypto_sign_SECRETKEYBYTES) + + if (seed) sodium.crypto_sign_seed_keypair(this.#publicKey, this.#secretKey, seed) + else sodium.crypto_sign_keypair(this.#publicKey, this.#secretKey) + } + + static random () { + const seed = randomBytes(32) + + return new KeyPair(seed) + } + + zeroize () { + zeroize(this.#secretKey) + this.secretKey = null + } + + publicKey () { + return new PublicKey(this.#publicKey) + } + + secretKey () { + return this.#secretKey + } + + /** + * @param {Uint8Array} message + */ + sign (message) { + const signature = Buffer.alloc(sodium.crypto_sign_BYTES) + sodium.crypto_sign_detached(signature, message, this.#secretKey) + + return signature + } +} + +export class PublicKey { + /** + * @param {Buffer} bytes + */ + constructor (bytes) { + this.bytes = bytes + } + + /** + * @param {string} string + * @returns {Error | PublicKey} + */ + static fromString (string) { + if (string.length !== 52) { + return new Error('Invalid PublicKey string, expected 52 characters, got: ' + string.length) + } + + try { + return new PublicKey(z32.decode(string)) + } catch (error) { + return error + } + } + + /** + * @param {Buffer} signature + * @param {Buffer} message + */ + verify (signature, message) { + const valid = sodium.crypto_sign_verify_detached(signature, message, this.bytes) + if (!valid) return new Error('Invalid signature') + + return true + } + + toString () { + return z32.encode(this.bytes) + } +} diff --git a/js/pubky/src/common/index.js b/js/pubky/src/common/index.js new file mode 100644 index 0000000..71ccb60 --- /dev/null +++ b/js/pubky/src/common/index.js @@ -0,0 +1,8 @@ +export * as crypto from './crypto.js' +export { Timestamp } from './timestamp.js' +export { AuthnSignature, AuthnVerifier } from './auth.js' + +/** + * @typedef {string | number | boolean | null} JSONValue + * @typedef {{[key: string]: JSONValue | Array}} JSONObject + */ diff --git a/js/pubky/src/common/namespaces.js b/js/pubky/src/common/namespaces.js new file mode 100644 index 0000000..156d3af --- /dev/null +++ b/js/pubky/src/common/namespaces.js @@ -0,0 +1 @@ +export const PUBKY_AUTHN = Buffer.from('PUBKY:AUTHN') diff --git a/js/pubky/src/common/timestamp.js b/js/pubky/src/common/timestamp.js new file mode 100644 index 0000000..eeeadec --- /dev/null +++ b/js/pubky/src/common/timestamp.js @@ -0,0 +1,53 @@ +import { CrockfordBase32 } from 'crockford-base32' +import { randomBytes } from './crypto.js' + +const clockId = randomBytes(1).readUintBE(0, 1) +let latest = 0 + +export class Timestamp { + /** + * @param {number} microseconds - u64 microseconds + */ + constructor (microseconds) { + /** microseconds as u64 */ + this.microseconds = microseconds + } + + static now () { + const now = Date.now() + latest = Math.max(now, latest + 1) + + return new Timestamp((latest * 1000) + clockId) + } + + /** + * @param {string} string + */ + static fromString (string) { + const microseconds = Number(CrockfordBase32.decode(string, { asNumber: true })) + return new Timestamp(microseconds) + } + + /** + * @param {Date} date + */ + static fromDate (date) { + const microseconds = Number(date) * 1000 + return new Timestamp(microseconds) + } + + toString () { + return CrockfordBase32.encode(this.microseconds) + } + + toDate () { + return new Date(this.microseconds / 1000) + } + + intoBytes () { + const buffer = Buffer.allocUnsafe(8) + buffer.writeBigUint64BE(BigInt(this.microseconds), 0) + + return buffer + } +} diff --git a/js/pubky/tsconfig.json b/js/pubky/tsconfig.json new file mode 100644 index 0000000..63d7a71 --- /dev/null +++ b/js/pubky/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + // Declarations control + "target": "esnext", + "module": "esnext", + + "noEmitOnError": true, + "emitDeclarationOnly": true, + "declarationMap": true, + "isolatedModules": true, + + "incremental": true, + "composite": true, + + // Check control + "strict": false, + "allowJs": true, + "checkJs": true, + + // module resolution + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + + // advanced + "verbatimModuleSyntax": true, + "skipLibCheck": true, + + "outDir": "types" + }, + "include": ["src", "lib"] +} From ae4a34c511b04a9d266ab344cbef2fa658464d1a Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 24 Jul 2024 22:26:32 +0300 Subject: [PATCH 007/125] feat(pubky): add wasm mod --- Cargo.lock | 5 ++ js/pubky/README.md | 3 + js/pubky/examples/basic.js | 9 +++ js/pubky/src/index.js | 5 ++ js/pubky/src/lib/client.js | 102 ++++++++++++++++++++++++++++++ js/pubky/src/lib/error.js | 1 + js/pubky/src/lib/fetch-browser.js | 11 ++++ js/pubky/src/lib/fetch.js | 3 + pubky/Cargo.toml | 21 +++++- pubky/src/lib.rs | 32 ++++++++-- pubky/src/wasm.rs | 6 ++ pubky/src/wasm/client.rs | 73 +++++++++++++++++++++ pubky/src/wasm/keys.rs | 15 +++++ 13 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 js/pubky/README.md create mode 100644 js/pubky/examples/basic.js create mode 100644 js/pubky/src/index.js create mode 100644 js/pubky/src/lib/client.js create mode 100644 js/pubky/src/lib/error.js create mode 100644 js/pubky/src/lib/fetch-browser.js create mode 100644 js/pubky/src/lib/fetch.js create mode 100644 pubky/src/wasm.rs create mode 100644 pubky/src/wasm/client.rs create mode 100644 pubky/src/wasm/keys.rs diff --git a/Cargo.lock b/Cargo.lock index 613c9c4..910eaa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1252,6 +1252,8 @@ version = "0.1.0" dependencies = [ "bytes", "flume", + "futures", + "js-sys", "pkarr", "pubky-common", "pubky_homeserver", @@ -1259,6 +1261,9 @@ dependencies = [ "tokio", "ureq", "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/js/pubky/README.md b/js/pubky/README.md new file mode 100644 index 0000000..c57a08a --- /dev/null +++ b/js/pubky/README.md @@ -0,0 +1,3 @@ +# Pubky + +This is a JavaScript implementation of a Pubky protocol client-side tools. diff --git a/js/pubky/examples/basic.js b/js/pubky/examples/basic.js new file mode 100644 index 0000000..6b40582 --- /dev/null +++ b/js/pubky/examples/basic.js @@ -0,0 +1,9 @@ +import { PubkyClient } from '../src/index.js' + +main() + +async function main() { + let client = new PubkyClient() + + console.log(client) +} diff --git a/js/pubky/src/index.js b/js/pubky/src/index.js new file mode 100644 index 0000000..e3e7548 --- /dev/null +++ b/js/pubky/src/index.js @@ -0,0 +1,5 @@ +// import { PubkyClient as _PubkyClient } from './lib/client.js' +// import { PubkyError as _PubkyError } from './lib/error.js' +// +// export const PubkyClient = _PubkyClient; +// export const PubkyError = _PubkyError; diff --git a/js/pubky/src/lib/client.js b/js/pubky/src/lib/client.js new file mode 100644 index 0000000..742262d --- /dev/null +++ b/js/pubky/src/lib/client.js @@ -0,0 +1,102 @@ +import Pkarr, { SignedPacket } from 'pkarr' +import { URL } from 'url' + +// import { AuthnSignature, crypto } from '@pubky/common' + +import * as crypto from '../common/crypto.js' + +// import { Pubky } from './pubky.js' +import fetch from './fetch.js' + + +const DEFAULT_PKARR_RELAY = new URL('https://relay.pkarr.org') + +export class PubkyClient { + // TODO: use DHT in nodejs + #pkarrRelay + + crypto = crypto + static crypto = crypto + + /** + * @param {object} [options={}] + * @param {URL} [options.pkarrRelay] + * + * @param {{relay: string, bootstrap: Array<{host: string, port: number}>}} [options.testnet] + */ + constructor(options = {}) { + this.#pkarrRelay = options.pkarrRelay || DEFAULT_PKARR_RELAY + } + + /** + * Publish the SVCB record for `_pubky.`. + * @param {crypto.KeyPair} keypair + * @param {String} host + */ + async publishPubkyHomeserver(keypair, host) { + + let existing = await (async () => { + try { + return (await Pkarr.relayGet(this.#pkarrRelay.toString(), keypair.publicKey().bytes)).packet() + } catch (error) { + return { + id: 0, + type: 'response', + flags: 0, + answers: [] + } + } + })(); + + let answers = [ + ]; + + for (let answer of existing.answers) { + if (!answer.name.startsWith("_pubky")) { + answers.push(answer) + } + } + + let signedPacket = SignedPacket.fromPacket(keypair, { + id: 0, + type: 'response', + flags: 0, + answers: [ + ...answers, + { + name: '_pubky.', type: 'SVCB', ttl: 7200, data: + + Buffer.from( + + ) + + } + ] + }) + + // let mut packet = Packet:: new_reply(0); + // + // if let Some(existing) = self.pkarr.resolve(& keypair.public_key()) ? { + // for answer in existing.packet().answers.iter().cloned() { + // if !answer.name.to_string().starts_with("_pubky") { + // packet.answers.push(answer.into_owned()) + // } + // } + // } + // + // let svcb = SVCB::new (0, host.try_into() ?); + // + // packet.answers.push(pkarr:: dns:: ResourceRecord:: new ( + // "_pubky".try_into().unwrap(), + // pkarr:: dns:: CLASS:: IN, + // 60 * 60, + // pkarr:: dns:: rdata:: RData:: SVCB(svcb), + // )); + // + // let signed_packet = SignedPacket:: from_packet(keypair, & packet) ?; + // + // self.pkarr.publish(& signed_packet) ?; + } +} + +export default PubkyClient diff --git a/js/pubky/src/lib/error.js b/js/pubky/src/lib/error.js new file mode 100644 index 0000000..29d4146 --- /dev/null +++ b/js/pubky/src/lib/error.js @@ -0,0 +1 @@ +export class PubkyError extends Error { } diff --git a/js/pubky/src/lib/fetch-browser.js b/js/pubky/src/lib/fetch-browser.js new file mode 100644 index 0000000..30b547c --- /dev/null +++ b/js/pubky/src/lib/fetch-browser.js @@ -0,0 +1,11 @@ +/* eslint-disable no-prototype-builtins */ +const g = + (typeof globalThis !== 'undefined' && globalThis) || + // eslint-disable-next-line no-undef + (typeof self !== 'undefined' && self) || + // eslint-disable-next-line no-undef + (typeof global !== 'undefined' && global) || + {} + +// @ts-ignore +export default g.fetch diff --git a/js/pubky/src/lib/fetch.js b/js/pubky/src/lib/fetch.js new file mode 100644 index 0000000..7c24a8b --- /dev/null +++ b/js/pubky/src/lib/fetch.js @@ -0,0 +1,3 @@ +import fetch from 'node-fetch-cache' + +export default fetch diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index c40cd9c..57a8b5c 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -3,16 +3,35 @@ name = "pubky" version = "0.1.0" edition = "2021" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] +pkarr = "2.1.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } -pkarr = "2.1.0" ureq = { version = "2.10.0", features = ["cookies"] } thiserror = "1.0.62" url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" +[target.'cfg(target_arch = "wasm32")'.dependencies] +futures = "0.3.29" +js-sys = "0.3.69" +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" +web-sys = { version = "0.3.69", features = [ + "console", + "Request", + "RequestInit", + "RequestMode", + "Response", + "Window", +] } + [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 7125ca1..4ed3714 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -1,8 +1,30 @@ #![allow(unused)] -mod client; -mod client_async; -mod error; +macro_rules! if_not_wasm { + ($($item:item)*) => {$( + #[cfg(not(target_arch = "wasm32"))] + $item + )*} +} -pub use client::PubkyClient; -pub use error::Error; +macro_rules! if_wasm { + ($($item:item)*) => {$( + #[cfg(target_arch = "wasm32")] + $item + )*} +} + +if_not_wasm! { + mod client; + mod client_async; + mod error; + + pub use client::PubkyClient; + pub use error::Error; +} + +if_wasm! { +mod wasm; + +pub use wasm::{PubkyClient, Keypair}; +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs new file mode 100644 index 0000000..faddf5b --- /dev/null +++ b/pubky/src/wasm.rs @@ -0,0 +1,6 @@ +mod keys; + +mod client; + +pub use client::PubkyClient; +pub use keys::Keypair; diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/client.rs new file mode 100644 index 0000000..7504789 --- /dev/null +++ b/pubky/src/wasm/client.rs @@ -0,0 +1,73 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use web_sys::RequestMode; + +use pkarr::PkarrRelayClient; + +use super::Keypair; + +#[wasm_bindgen] +pub struct Error {} + +#[wasm_bindgen] +pub struct PubkyClient { + pkarr: PkarrRelayClient, +} + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + pkarr: PkarrRelayClient::default(), + } + } + + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + #[wasm_bindgen] + pub fn signup(&self, secret_key: Keypair, homeserver: &str) -> Result<(), JsValue> { + // let (audience, mut url) = self.resolve_endpoint(homeserver)?; + + // url.set_path(&format!("/{}", keypair.public_key())); + + // let body = AuthnSignature::generate(keypair, &audience).as_bytes(); + + // fetch_base(url.to_string(), "PUT", body).await?; + + // self.publish_pubky_homeserver(keypair, homeserver); + + Ok(()) + } +} + +async fn fetch_base( + url: &String, + method: &str, + body: Option>, +) -> Result { + let mut opts = web_sys::RequestInit::new(); + opts.method(method); + opts.mode(RequestMode::Cors); + + if let Some(body) = body { + let body_bytes: &[u8] = &body; + let body_array: js_sys::Uint8Array = body_bytes.into(); + let js_value: &JsValue = body_array.as_ref(); + opts.body(Some(js_value)); + } + + let js_request = web_sys::Request::new_with_str_and_init(url, &opts)?; + // .map_err(|error| Error::JsError(error))?; + + let window = web_sys::window().unwrap(); + let response = JsFuture::from(window.fetch_with_request(&js_request)).await?; + // .map_err(|error| Error::JsError(error))?; + + let response: web_sys::Response = response.dyn_into()?; + // .map_err(|error| Error::JsError(error))? + + Ok(response) +} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs new file mode 100644 index 0000000..3859511 --- /dev/null +++ b/pubky/src/wasm/keys.rs @@ -0,0 +1,15 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Keypair(pkarr::Keypair); + +#[wasm_bindgen] +impl Keypair { + #[wasm_bindgen] + pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { + let mut bytes = [0; 32]; + secret_key.copy_to(&mut bytes); + + Self(pkarr::Keypair::from_secret_key(&bytes)) + } +} From b03602045f212d81510a55e0e125354e34c629ea Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 25 Jul 2024 14:05:42 +0300 Subject: [PATCH 008/125] feat(js): successful _initial_ test of wasm in nodejs and browser --- Cargo.lock | 42 ++++++ examples/nodejs/.gitignore | 5 + examples/nodejs/README.md | 3 + examples/nodejs/index.js | 10 ++ examples/nodejs/package.json | 7 + examples/web/auth/.gitignore | 5 + examples/web/auth/index.html | 38 +++++ examples/web/auth/package.json | 23 +++ examples/web/auth/package.json0 | 30 ++++ .../auth/src/components/RecoveryFileUpload.js | 78 ++++++++++ examples/web/auth/src/components/layout.jsx | 21 +++ examples/web/auth/src/index.jsx | 21 +++ examples/web/auth/src/pages/Home.jsx | 43 ++++++ examples/web/auth/src/store.js | 90 ++++++++++++ examples/web/auth/src/style.css | 137 ++++++++++++++++++ examples/web/auth/vite.config.js | 12 ++ examples/web/no-bundler/.gitignore | 3 + examples/web/no-bundler/README.md | 3 + examples/web/no-bundler/index.html | 46 ++++++ examples/web/no-bundler/package.json | 17 +++ examples/web/package-lock.json | 6 + pubky/Cargo.toml | 10 ++ pubky/pkg/.gitignore | 2 + pubky/pkg/LICENSE | 21 +++ pubky/pkg/README.md | 3 + pubky/pkg/package.json | 28 ++++ pubky/src/bin/bundle_wasm.rs | 58 ++++++++ pubky/src/bin/patch.mjs | 54 +++++++ pubky/src/lib.rs | 4 +- pubky/src/wasm/client.rs | 17 ++- 30 files changed, 832 insertions(+), 5 deletions(-) create mode 100644 examples/nodejs/.gitignore create mode 100644 examples/nodejs/README.md create mode 100644 examples/nodejs/index.js create mode 100644 examples/nodejs/package.json create mode 100644 examples/web/auth/.gitignore create mode 100644 examples/web/auth/index.html create mode 100644 examples/web/auth/package.json create mode 100644 examples/web/auth/package.json0 create mode 100644 examples/web/auth/src/components/RecoveryFileUpload.js create mode 100644 examples/web/auth/src/components/layout.jsx create mode 100644 examples/web/auth/src/index.jsx create mode 100644 examples/web/auth/src/pages/Home.jsx create mode 100644 examples/web/auth/src/store.js create mode 100644 examples/web/auth/src/style.css create mode 100644 examples/web/auth/vite.config.js create mode 100644 examples/web/no-bundler/.gitignore create mode 100644 examples/web/no-bundler/README.md create mode 100644 examples/web/no-bundler/index.html create mode 100644 examples/web/no-bundler/package.json create mode 100644 examples/web/package-lock.json create mode 100644 pubky/pkg/.gitignore create mode 100644 pubky/pkg/LICENSE create mode 100644 pubky/pkg/README.md create mode 100644 pubky/pkg/package.json create mode 100644 pubky/src/bin/bundle_wasm.rs create mode 100644 pubky/src/bin/patch.mjs diff --git a/Cargo.lock b/Cargo.lock index 910eaa2..b8b2d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,6 +265,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1263,6 +1273,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-bindgen-test", "web-sys", ] @@ -1482,6 +1493,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2119,6 +2136,31 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-bindgen-test" +version = "0.3.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "web-sys" version = "0.3.69" diff --git a/examples/nodejs/.gitignore b/examples/nodejs/.gitignore new file mode 100644 index 0000000..fb59162 --- /dev/null +++ b/examples/nodejs/.gitignore @@ -0,0 +1,5 @@ +.netlify +.parcel* +dist +node_modules +package-lock.json diff --git a/examples/nodejs/README.md b/examples/nodejs/README.md new file mode 100644 index 0000000..c427745 --- /dev/null +++ b/examples/nodejs/README.md @@ -0,0 +1,3 @@ +# Nodejs + +An example of using Pubky wasm in nodejs diff --git a/examples/nodejs/index.js b/examples/nodejs/index.js new file mode 100644 index 0000000..06acbaa --- /dev/null +++ b/examples/nodejs/index.js @@ -0,0 +1,10 @@ +import { PubkyClient, Keypair } from '@synonymdev/pubky' + +let keypair = Keypair.from_secret_key(new Uint8Array(32).fill(0)) +console.log(keypair) + +const client = new PubkyClient() + +console.log(client) + +const x = client.signup(keypair, "foo.com") diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json new file mode 100644 index 0000000..5e17eb5 --- /dev/null +++ b/examples/nodejs/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "main": "index.js", + "dependencies": { + "@synonymdev/pubky":"file:../../pubky/pkg" + } +} diff --git a/examples/web/auth/.gitignore b/examples/web/auth/.gitignore new file mode 100644 index 0000000..fb59162 --- /dev/null +++ b/examples/web/auth/.gitignore @@ -0,0 +1,5 @@ +.netlify +.parcel* +dist +node_modules +package-lock.json diff --git a/examples/web/auth/index.html b/examples/web/auth/index.html new file mode 100644 index 0000000..ef33b63 --- /dev/null +++ b/examples/web/auth/index.html @@ -0,0 +1,38 @@ + + + + + + + + Pubky demo + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/examples/web/auth/package.json b/examples/web/auth/package.json new file mode 100644 index 0000000..6f1660b --- /dev/null +++ b/examples/web/auth/package.json @@ -0,0 +1,23 @@ +{ + "private": "true", + "main": "src/index.js", + "type": "module", + "scripts": { + "build": "vite build", + "start": "vite serve", + "preview": "vite preview", + "wasm": "wasm-pack build ../../../pubky/ --target web" + }, + "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", + + "@solidjs/router": "^0.10.9", + "solid-js": "^1.7.0", + "vite-plugin-solid": "^2.10.2" + }, + "devDependencies": { + "typescript": "^4.9.5", + "vite": "^4.1.4", + "vite-plugin-html": "^3.2.0" + } +} diff --git a/examples/web/auth/package.json0 b/examples/web/auth/package.json0 new file mode 100644 index 0000000..9bd267d --- /dev/null +++ b/examples/web/auth/package.json0 @@ -0,0 +1,30 @@ +{ + "private": "true", + "type": "module", + "source": "src/index.html", + "browserslist": "> 0.5%, last 2 versions, not dead", + "scripts": { + "start": "parcel -p 7251", + "serve": "npm run start", + "build": "parcel build", + "lint": "standard --fix" + }, + "standard": { + "ignore": [ + "src/pages", + "src/components", + "dist" + ] + }, + "dependencies": { + "@solidjs/router": "^0.10.9", + "parcel": "^2.0.1", + "@synonymdev/pubky": "file:../../../pubky/pkg", + "solid-js": "^1.7.0" + }, + "devDependencies": { + "@babel/core": "^7.24.9", + "babel-preset-solid": "^1.2.5", + "solid-refresh": "^0.2.2" + } +} diff --git a/examples/web/auth/src/components/RecoveryFileUpload.js b/examples/web/auth/src/components/RecoveryFileUpload.js new file mode 100644 index 0000000..06b3ad0 --- /dev/null +++ b/examples/web/auth/src/components/RecoveryFileUpload.js @@ -0,0 +1,78 @@ +import { useNavigate } from '@solidjs/router' +import { createSignal } from 'solid-js' +import { crypto } from '@pubky/common' + +import { decryptRecoveryFile } from '../sdk/recovery.js' +import store from '../store.js' + +/** + * @param {"login" | "signup"} type + */ +const RecoveryFileUpload = ({ type }) => { + const [valid, setValid] = createSignal(false) + + const navigate = useNavigate() + + const onSubmit = async (e) => { + e.preventDefault() + + const form = e.target + + const file = form.file.files[0] + const passphrase = form['import-passphrase'].value + + const reader = new FileReader() + + reader.onload = async function(event) { + const recoveryFile = event.target.result + + const seedResult = await decryptRecoveryFile(recoveryFile, passphrase) + if (seedResult.isErr()) return alert(seedResult.error.message) + + const action = type === 'signup' + ? store.pubkyClient.signup.bind(store.pubkyClient) + : store.pubkyClient.login.bind(store.pubkyClient); + + const result = await action(seedResult.value) + crypto.zeroize(seedResult.value) + + if (result.isErr()) return alert(result.error.message) + + store.setCurrentUser({ id: result.value }) + + navigate("/", { replace: true }) + } + + // Read the file as text + reader.readAsText(file) + } + + const onUpdate = (e) => { + const form = e.target.parentElement.parentElement + + const file = form.file.files[0] + const passphrase = form['import-passphrase'].value + + if (passphrase.length > 0 && file) { + setValid(true) + } else { + setValid(false) + } + } + + return ( +
+ + + +
+ ) +} + +export default RecoveryFileUpload diff --git a/examples/web/auth/src/components/layout.jsx b/examples/web/auth/src/components/layout.jsx new file mode 100644 index 0000000..1d0c03b --- /dev/null +++ b/examples/web/auth/src/components/layout.jsx @@ -0,0 +1,21 @@ +const Layout = ({ children }) => { + return ( + <> +
+
+

Pubky

+
+
+ +
+ {children} +
+ +
+

This is a proof of concept for demonstration purposes only.

+
+ + ) +} + +export default Layout diff --git a/examples/web/auth/src/index.jsx b/examples/web/auth/src/index.jsx new file mode 100644 index 0000000..1c78d71 --- /dev/null +++ b/examples/web/auth/src/index.jsx @@ -0,0 +1,21 @@ +import { render } from 'solid-js/web' +import { Router, Route } from '@solidjs/router' + +import { PubkyClient } from "@synonymdev/pubky"; + +let client = new PubkyClient() +console.log(client); + +import Home from './pages/Home.jsx' +// import Login from './pages/Login.js' +// import Signup from './pages/Signup.js' + +render(() => ( + + + +), document.getElementById('app')) + +// +// +// diff --git a/examples/web/auth/src/pages/Home.jsx b/examples/web/auth/src/pages/Home.jsx new file mode 100644 index 0000000..307d1a1 --- /dev/null +++ b/examples/web/auth/src/pages/Home.jsx @@ -0,0 +1,43 @@ +import { useNavigate } from '@solidjs/router' + +import store from '../store.js' +import Layout from '../components/Layout' + +const Home = () => { + const navigate = useNavigate() + + let currentUser = store.getCurrentUser() + + if (!currentUser) { + navigate('/login', { replace: true }) + } + + const logout = async () => { + // await store.pubkyClient.ready() + // + // const logoutResult = await store.pubkyClient.logout(currentUser.id) + // if (logoutResult.isErr()) { + // alert(logoutResult.error.message) + // return + // } + // + // store.removeCurrentUser() + // + // if (window.location.pathname === '/home') { + // navigate('/', { replace: true }) + // } else { + // navigate('/home', { replace: true }) + // } + } + + return ( + + Home.. +

Welcome {store.getCurrentUser()?.id}

+
+ +
+ ) +} + +export default Home diff --git a/examples/web/auth/src/store.js b/examples/web/auth/src/store.js new file mode 100644 index 0000000..910d6a5 --- /dev/null +++ b/examples/web/auth/src/store.js @@ -0,0 +1,90 @@ +import { createMutable } from 'solid-js/store' +// import z32 from 'z32' +// import { Result } from '@pubky/common' +// import { Level } from 'level' +// import Client from '@pubky/client' +// +// import { recoveryFile } from './sdk/recovery.js' +// +// // In real application it should be the server's Pkarr Id +// const DEFAULT_HOME_SERVER = 'http://localhost:7259' +// const DEFAULT_RELAY = 'https://relay.pkarr.org' + +class Store { + constructor() { + // this.db = new Level('app-db', { keyEncoding: 'utf8', valueEncoding: 'json' }) + // this.currentUser = null + // + // this.pubkyClient = new Client(DEFAULT_HOME_SERVER, { relay: DEFAULT_RELAY }) + // + // this.DEFAULT_HOME_SERVER = DEFAULT_HOME_SERVER + } + + // getUsers() { + // try { + // return JSON.parse(global.localStorage.getItem('users')) || [] + // } catch { + // return [] + // } + // } + + getCurrentUser() { + try { + return JSON.parse(global.localStorage.getItem('currentUser')) + } catch { + return null + } + } + + // setCurrentUser(user) { + // const users = this.getUsers() + // + // if (!users?.map(user => user.id).includes(user.id)) { + // global.localStorage.setItem('users', JSON.stringify([ + // ...users, + // user + // ])) + // } + // + // global.localStorage.setItem('currentUser', JSON.stringify(user)) + // } + // + // removeCurrentUser() { + // global.localStorage.removeItem('currentUser') + // } + // + // /** + // * @param {string} name + // * @param {string} passphrase + // * + // * @returns {Promise>} + // */ + // async createAccount(name, passphrase) { + // await this.pubkyClient.ready() + // + // const seed = Client.crypto.generateSeed() + // + // const keypair = Client.crypto.generateKeyPair(seed) + // Client.crypto.zeroize(keypair.secretKey) + // + // const userId = z32.encode(keypair.publicKey) + // + // const recoveryFileAndFilename = await recoveryFile(name, seed, passphrase) + // + // const signedUp = await this.pubkyClient.signup(seed) + // if (signedUp.isErr()) return signedUp + // + // Client.crypto.zeroize(seed) + // + // return Result.Ok({ + // userId, + // ...recoveryFileAndFilename + // }) + // } +} + +export default createMutable(new Store()) diff --git a/examples/web/auth/src/style.css b/examples/web/auth/src/style.css new file mode 100644 index 0000000..26c3d1d --- /dev/null +++ b/examples/web/auth/src/style.css @@ -0,0 +1,137 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + min-width: 320px; +} + +body, +#app { + display: flex; + flex-direction: column; + justify-content: space-between; + max-width: 600px; + margin: 0 auto; + padding: 2rem 1rem 0; + font-family: 'IBM Plex Sans', Helvetica, sans-serif; +} + +.button { + background: none; + border: none; + cursor: pointer; +} + +.button:disabled { + pointer-events: none; + opacity: 0.4; +} + +main { + height: 100%; + margin: 1rem 0; +} + +footer { + text-align: center; + color: #666; + padding: 1rem 0; + border-top: 1px solid #ddd; + margin-top: 1rem; +} + +footer p, +footer a { + font-size: 0.8rem !important; + padding-bottom: 0.3rem; +} + +.row { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +a { + color: #000; + font-size: 1rem; + font-weight: 700; +} + +h1 { + font-size: 2rem; + font-weight: 700; +} + +.small { + font-size: 0.8rem; +} + +.button.primary { + background: black; + color: white; + + min-width: 90px; + width: 100%; + + padding: 0.4rem 0.8rem; + border: 2px solid #000; +} + +.button.primary:hover { + background: #222; +} + +.divider { + background: #333; + height: 1px; + margin-top: 1rem; + margin-bottom: 1rem; +} + +form { + display: flex; + flex-direction: column; +} + +label { + font-size: 0.9rem; + font-weight: 700; + margin-bottom: 1rem; +} + +form input { + font-size: 1rem; + width: 100%; + height: 2rem; + border: none; + box-shadow: none; + border-radius: 0; + border-bottom: 1px solid #ddd; +} + +form input:focus { + outline: none; + border-bottom: 2px solid #000; +} + +label.checkbox { + display: flex; + font-weight: normal; + line-height: 1rem; +} + +label.checkbox input { + width: 1rem; + margin: 0 0.5rem 0 0; +} +label.checkbox:focus-within { + outline: 1px solid; +} diff --git a/examples/web/auth/vite.config.js b/examples/web/auth/vite.config.js new file mode 100644 index 0000000..6dba9ca --- /dev/null +++ b/examples/web/auth/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +// import wasmPack from "vite-plugin-wasm-pack"; +import solidPlugin from 'vite-plugin-solid'; +// import path from "node:path"; + +export default defineConfig({ + // pass your local crate path to the plugin + plugins: [ + // wasmPack(path.resolve("../../../pubky")), + solidPlugin() + ], +}); diff --git a/examples/web/no-bundler/.gitignore b/examples/web/no-bundler/.gitignore new file mode 100644 index 0000000..91a3983 --- /dev/null +++ b/examples/web/no-bundler/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +package-lock.json diff --git a/examples/web/no-bundler/README.md b/examples/web/no-bundler/README.md new file mode 100644 index 0000000..ddd6280 --- /dev/null +++ b/examples/web/no-bundler/README.md @@ -0,0 +1,3 @@ +# No bundler + +An example of using Pubky wasm immidiatly in an `index.html` with no bundlers. diff --git a/examples/web/no-bundler/index.html b/examples/web/no-bundler/index.html new file mode 100644 index 0000000..0770e09 --- /dev/null +++ b/examples/web/no-bundler/index.html @@ -0,0 +1,46 @@ + + + + + + + + Pubky demo + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/examples/web/no-bundler/package.json b/examples/web/no-bundler/package.json new file mode 100644 index 0000000..e6e1195 --- /dev/null +++ b/examples/web/no-bundler/package.json @@ -0,0 +1,17 @@ +{ + "scripts": { + "start": "vite serve" + }, + "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", + + "@solidjs/router": "^0.10.9", + "solid-js": "^1.7.0" + }, + "devDependencies": { + "typescript": "^4.9.5", + "vite": "^4.1.4", + "vite-plugin-html": "^3.2.0", + "vite-plugin-solid": "^2.10.2" + } +} diff --git a/examples/web/package-lock.json b/examples/web/package-lock.json new file mode 100644 index 0000000..29993b6 --- /dev/null +++ b/examples/web/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "web", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 57a8b5c..e3f6e93 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -2,6 +2,10 @@ name = "pubky" version = "0.1.0" edition = "2021" +description = "Pubky client" +license = "MIT" +repository = "https://github.com/pubky/pubky" +keywords = ["web", "dht", "dns", "decentralized", "identity"] [lib] crate-type = ["cdylib", "rlib"] @@ -36,7 +40,13 @@ web-sys = { version = "0.3.69", features = [ pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3.42" + [features] async = ["flume/async"] default = ["async"] + +[package.metadata.docs.rs] +all-features = true diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore new file mode 100644 index 0000000..7a5774d --- /dev/null +++ b/pubky/pkg/.gitignore @@ -0,0 +1,2 @@ +nodejs/* +pubky.mjs diff --git a/pubky/pkg/LICENSE b/pubky/pkg/LICENSE new file mode 100644 index 0000000..a0e67c5 --- /dev/null +++ b/pubky/pkg/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md new file mode 100644 index 0000000..32080e5 --- /dev/null +++ b/pubky/pkg/README.md @@ -0,0 +1,3 @@ +# Pubky + +JavaScript implementation of [Pubky](https://github.com/pubky/pubky). diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json new file mode 100644 index 0000000..6398be1 --- /dev/null +++ b/pubky/pkg/package.json @@ -0,0 +1,28 @@ +{ + "name": "@synonymdev/pubky", + "type": "module", + "description": "Pubky client", + "version": "0.1.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/pubky/pubky" + }, + "files": [ + "nodejs/*", + "pupky.mjs" + ], + "main": "nodejs/pubky.js", + "browser": "pubky.mjs", + "types": "pubky.d.ts", + "sideEffects": [ + "./snippets/*" + ], + "keywords": [ + "web", + "dht", + "dns", + "decentralized", + "identity" + ] +} diff --git a/pubky/src/bin/bundle_wasm.rs b/pubky/src/bin/bundle_wasm.rs new file mode 100644 index 0000000..44cf304 --- /dev/null +++ b/pubky/src/bin/bundle_wasm.rs @@ -0,0 +1,58 @@ +use std::io; +use std::process::{Command, ExitStatus}; + +// If the process hangs, try `cargo clean` to remove all locks. + +fn main() { + println!("cargo:rerun-if-changed=client/"); + + build_wasm("nodejs").unwrap(); + patch().unwrap(); +} + +fn build_wasm(target: &str) -> io::Result { + let output = Command::new("wasm-pack") + .args([ + "build", + "--release", + "--target", + target, + "--out-dir", + &format!("pkg/{}", target), + ]) + .output()?; + + println!( + "wasm-pack {target} output: {}", + String::from_utf8_lossy(&output.stdout) + ); + + if !output.status.success() { + eprintln!( + "wasm-pack failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output.status) +} + +fn patch() -> io::Result { + let output = Command::new("node") + .args(["./src/bin/patch.mjs"]) + .output()?; + + println!( + "patch.mjs output: {}", + String::from_utf8_lossy(&output.stdout) + ); + + if !output.status.success() { + eprintln!( + "wasm-pack failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output.status) +} diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs new file mode 100644 index 0000000..554c6c8 --- /dev/null +++ b/pubky/src/bin/patch.mjs @@ -0,0 +1,54 @@ +// This script is used to generate isomorphic code for web and nodejs +// +// Based on hacks from [this issue](https://github.com/rustwasm/wasm-pack/issues/1334) + +import { readFile, writeFile } from "node:fs/promises"; + +const cargoTomlContent = await readFile("./Cargo.toml", "utf8"); +const cargoPackageName = /\[package\]\nname = "(.*?)"/.exec(cargoTomlContent)[1] +const name = cargoPackageName.replace(/-/g, '_') + +const content = await readFile(`./pkg/nodejs/${name}.js`, "utf8"); + +const patched = content + // use global TextDecoder TextEncoder + .replace("require(`util`)", "globalThis") + // attach to `imports` instead of module.exports + .replace("= module.exports", "= imports") + + // add suffix Class + .replace(/\nclass (.*?) \{/g, "\nclass $1Class {") + .replace(/\nmodule\.exports\.(.*?) = (.*?);/g, "\nexport const $1 = imports.$1 = $1Class") + + // quick and dirty fix for a bug caused by the previous replace + .replace(/__wasmClass/g, "wasm") + + .replace(/\nmodule\.exports\.(.*?)\s+/g, "\nexport const $1 = imports.$1 ") + .replace(/$/, 'export default imports') + // inline bytes Uint8Array + .replace( + /\nconst path.*\nconst bytes.*\n/, + ` +var __toBinary = /* @__PURE__ */ (() => { + var table = new Uint8Array(128); + for (var i = 0; i < 64; i++) + table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i; + return (base64) => { + var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0); + for (var i2 = 0, j = 0; i2 < n; ) { + var c0 = table[base64.charCodeAt(i2++)], c1 = table[base64.charCodeAt(i2++)]; + var c2 = table[base64.charCodeAt(i2++)], c3 = table[base64.charCodeAt(i2++)]; + bytes[j++] = c0 << 2 | c1 >> 4; + bytes[j++] = c1 << 4 | c2 >> 2; + bytes[j++] = c2 << 6 | c3; + } + return bytes; + }; +})(); + +const bytes = __toBinary(${JSON.stringify(await readFile(`./pkg/nodejs/${name}_bg.wasm`, "base64")) + }); +`, + ); + +await writeFile(`./pkg/${name}.mjs`, patched); diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 4ed3714..afa33b6 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -24,7 +24,7 @@ if_not_wasm! { } if_wasm! { -mod wasm; + mod wasm; -pub use wasm::{PubkyClient, Keypair}; + pub use wasm::{PubkyClient, Keypair}; } diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/client.rs index 7504789..550c8e5 100644 --- a/pubky/src/wasm/client.rs +++ b/pubky/src/wasm/client.rs @@ -60,14 +60,25 @@ async fn fetch_base( } let js_request = web_sys::Request::new_with_str_and_init(url, &opts)?; - // .map_err(|error| Error::JsError(error))?; let window = web_sys::window().unwrap(); let response = JsFuture::from(window.fetch_with_request(&js_request)).await?; - // .map_err(|error| Error::JsError(error))?; let response: web_sys::Response = response.dyn_into()?; - // .map_err(|error| Error::JsError(error))? Ok(response) } + +#[cfg(test)] +mod tests { + use wasm_bindgen_test::*; + + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + use super::*; + + #[wasm_bindgen_test] + async fn basic() { + // let client = PubkyClient::new(); + } +} From e407461c0dcd70a6f69f488c91f3f48f0b317cb9 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 25 Jul 2024 14:36:39 +0300 Subject: [PATCH 009/125] examples: add preinstall script --- examples/nodejs/package.json | 3 +++ examples/web/auth/package.json | 2 +- examples/web/no-bundler/package.json | 3 ++- .../src/bin/{bundle_wasm.rs => bundle_pubky_npm.rs} | 11 +++++++++-- pubky/src/bin/patch.mjs | 13 +++++++++---- 5 files changed, 24 insertions(+), 8 deletions(-) rename pubky/src/bin/{bundle_wasm.rs => bundle_pubky_npm.rs} (76%) diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json index 5e17eb5..a2359bf 100644 --- a/examples/nodejs/package.json +++ b/examples/nodejs/package.json @@ -1,6 +1,9 @@ { "type": "module", "main": "index.js", + "scripts": { + "preinstall": "cargo run --bin bundle_pubky_npm" + }, "dependencies": { "@synonymdev/pubky":"file:../../pubky/pkg" } diff --git a/examples/web/auth/package.json b/examples/web/auth/package.json index 6f1660b..61e1abc 100644 --- a/examples/web/auth/package.json +++ b/examples/web/auth/package.json @@ -6,7 +6,7 @@ "build": "vite build", "start": "vite serve", "preview": "vite preview", - "wasm": "wasm-pack build ../../../pubky/ --target web" + "preinstall": "cargo run --bin bundle_pubky_npm" }, "dependencies": { "@synonymdev/pubky": "file:../../../pubky/pkg", diff --git a/examples/web/no-bundler/package.json b/examples/web/no-bundler/package.json index e6e1195..68d5fbc 100644 --- a/examples/web/no-bundler/package.json +++ b/examples/web/no-bundler/package.json @@ -1,6 +1,7 @@ { "scripts": { - "start": "vite serve" + "start": "vite serve", + "preinstall": "cargo run --bin bundle_pubky_npm" }, "dependencies": { "@synonymdev/pubky": "file:../../../pubky/pkg", diff --git a/pubky/src/bin/bundle_wasm.rs b/pubky/src/bin/bundle_pubky_npm.rs similarity index 76% rename from pubky/src/bin/bundle_wasm.rs rename to pubky/src/bin/bundle_pubky_npm.rs index 44cf304..b3305d3 100644 --- a/pubky/src/bin/bundle_wasm.rs +++ b/pubky/src/bin/bundle_pubky_npm.rs @@ -1,3 +1,4 @@ +use std::env; use std::io; use std::process::{Command, ExitStatus}; @@ -11,9 +12,12 @@ fn main() { } fn build_wasm(target: &str) -> io::Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + let output = Command::new("wasm-pack") .args([ "build", + &manifest_dir, "--release", "--target", target, @@ -38,8 +42,11 @@ fn build_wasm(target: &str) -> io::Result { } fn patch() -> io::Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + + println!("{manifest_dir}/src/bin/patch.mjs"); let output = Command::new("node") - .args(["./src/bin/patch.mjs"]) + .args([format!("{manifest_dir}/src/bin/patch.mjs")]) .output()?; println!( @@ -49,7 +56,7 @@ fn patch() -> io::Result { if !output.status.success() { eprintln!( - "wasm-pack failed: {}", + "patch.mjs failed: {}", String::from_utf8_lossy(&output.stderr) ); } diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs index 554c6c8..749b434 100644 --- a/pubky/src/bin/patch.mjs +++ b/pubky/src/bin/patch.mjs @@ -3,12 +3,17 @@ // Based on hacks from [this issue](https://github.com/rustwasm/wasm-pack/issues/1334) import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from 'node:url'; +import path, { dirname } from 'node:path'; -const cargoTomlContent = await readFile("./Cargo.toml", "utf8"); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const cargoTomlContent = await readFile(path.join(__dirname, "../../Cargo.toml"), "utf8"); const cargoPackageName = /\[package\]\nname = "(.*?)"/.exec(cargoTomlContent)[1] const name = cargoPackageName.replace(/-/g, '_') -const content = await readFile(`./pkg/nodejs/${name}.js`, "utf8"); +const content = await readFile(path.join(__dirname, `../../pkg/nodejs/${name}.js`), "utf8"); const patched = content // use global TextDecoder TextEncoder @@ -46,9 +51,9 @@ var __toBinary = /* @__PURE__ */ (() => { }; })(); -const bytes = __toBinary(${JSON.stringify(await readFile(`./pkg/nodejs/${name}_bg.wasm`, "base64")) +const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `../../pkg/nodejs/${name}_bg.wasm`), "base64")) }); `, ); -await writeFile(`./pkg/${name}.mjs`, patched); +await writeFile(path.join(__dirname, `../../pkg/${name}.mjs`), patched); From d81b6234d4a46885c372d5f7c5dd036919ccd87e Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 16:45:59 +0300 Subject: [PATCH 010/125] test: add JS unit tests instead of nodejs examples --- examples/nodejs/.gitignore | 5 - examples/nodejs/README.md | 3 - examples/nodejs/index.js | 10 -- examples/nodejs/package.json | 10 -- examples/web/package-lock.json | 6 - js/pubky/.gitignore | 6 - js/pubky/README.md | 3 - js/pubky/examples/basic.js | 9 -- js/pubky/package.json | 51 -------- js/pubky/src/common/auth.js | 194 ------------------------------ js/pubky/src/common/crypto.js | 131 -------------------- js/pubky/src/common/index.js | 8 -- js/pubky/src/common/namespaces.js | 1 - js/pubky/src/common/timestamp.js | 53 -------- js/pubky/src/index.js | 5 - js/pubky/src/lib/client.js | 102 ---------------- js/pubky/src/lib/error.js | 1 - js/pubky/src/lib/fetch-browser.js | 11 -- js/pubky/src/lib/fetch.js | 3 - js/pubky/tsconfig.json | 32 ----- pubky/pkg/.gitignore | 5 +- pubky/pkg/index.js | 1 + pubky/pkg/package.json | 18 ++- pubky/pkg/test/keys.js | 13 ++ pubky/src/bin/patch.mjs | 2 +- pubky/src/wasm/keys.rs | 32 ++++- 26 files changed, 64 insertions(+), 651 deletions(-) delete mode 100644 examples/nodejs/.gitignore delete mode 100644 examples/nodejs/README.md delete mode 100644 examples/nodejs/index.js delete mode 100644 examples/nodejs/package.json delete mode 100644 examples/web/package-lock.json delete mode 100644 js/pubky/.gitignore delete mode 100644 js/pubky/README.md delete mode 100644 js/pubky/examples/basic.js delete mode 100644 js/pubky/package.json delete mode 100644 js/pubky/src/common/auth.js delete mode 100644 js/pubky/src/common/crypto.js delete mode 100644 js/pubky/src/common/index.js delete mode 100644 js/pubky/src/common/namespaces.js delete mode 100644 js/pubky/src/common/timestamp.js delete mode 100644 js/pubky/src/index.js delete mode 100644 js/pubky/src/lib/client.js delete mode 100644 js/pubky/src/lib/error.js delete mode 100644 js/pubky/src/lib/fetch-browser.js delete mode 100644 js/pubky/src/lib/fetch.js delete mode 100644 js/pubky/tsconfig.json create mode 100644 pubky/pkg/index.js create mode 100644 pubky/pkg/test/keys.js diff --git a/examples/nodejs/.gitignore b/examples/nodejs/.gitignore deleted file mode 100644 index fb59162..0000000 --- a/examples/nodejs/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.netlify -.parcel* -dist -node_modules -package-lock.json diff --git a/examples/nodejs/README.md b/examples/nodejs/README.md deleted file mode 100644 index c427745..0000000 --- a/examples/nodejs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Nodejs - -An example of using Pubky wasm in nodejs diff --git a/examples/nodejs/index.js b/examples/nodejs/index.js deleted file mode 100644 index 06acbaa..0000000 --- a/examples/nodejs/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { PubkyClient, Keypair } from '@synonymdev/pubky' - -let keypair = Keypair.from_secret_key(new Uint8Array(32).fill(0)) -console.log(keypair) - -const client = new PubkyClient() - -console.log(client) - -const x = client.signup(keypair, "foo.com") diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json deleted file mode 100644 index a2359bf..0000000 --- a/examples/nodejs/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "module", - "main": "index.js", - "scripts": { - "preinstall": "cargo run --bin bundle_pubky_npm" - }, - "dependencies": { - "@synonymdev/pubky":"file:../../pubky/pkg" - } -} diff --git a/examples/web/package-lock.json b/examples/web/package-lock.json deleted file mode 100644 index 29993b6..0000000 --- a/examples/web/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "web", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/js/pubky/.gitignore b/js/pubky/.gitignore deleted file mode 100644 index 6da14b1..0000000 --- a/js/pubky/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -coverage -node_modules -types -.storage -.env -package-lock.json diff --git a/js/pubky/README.md b/js/pubky/README.md deleted file mode 100644 index c57a08a..0000000 --- a/js/pubky/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Pubky - -This is a JavaScript implementation of a Pubky protocol client-side tools. diff --git a/js/pubky/examples/basic.js b/js/pubky/examples/basic.js deleted file mode 100644 index 6b40582..0000000 --- a/js/pubky/examples/basic.js +++ /dev/null @@ -1,9 +0,0 @@ -import { PubkyClient } from '../src/index.js' - -main() - -async function main() { - let client = new PubkyClient() - - console.log(client) -} diff --git a/js/pubky/package.json b/js/pubky/package.json deleted file mode 100644 index 08a46cf..0000000 --- a/js/pubky/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@synonymdev/pubky", - "version": "0.1.0", - "description": "Pubky client library", - "type": "module", - "main": "src/index.js", - "types": "types/src/index.d.ts", - "repository": { - "type": "git", - "url": "git+https://github.com/slashtags/skunk-works.git" - }, - "scripts": { - "build": "tsc", - "clean": "rm -rf types", - "lint": "standard --fix", - "test": "brittle test/*.js -cov", - "depcheck": "npx depcheck --ignore-dirs=test", - "fullcheck": "npm run lint && npm run clean && npm run build && npm run test && npm run depcheck", - "prepublishOnly": "npm run fullcheck" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/pubky/pubky/issues" - }, - "homepage": "https://github.com/pubky/pubky/tree/master/js/pubky/#readme", - "files": [ - "src", - "types", - "!**/*.tsbuildinfo" - ], - "dependencies": { - "blake3-wasm": "^3.0.0", - "crockford-base32": "^2.0.0", - "eventsource": "^2.0.2", - "hash-wasm": "^4.11.0", - "node-fetch-cache": "^4.1.2", - "pkarr": "^1.4.1", - "z32": "^1.1.0" - }, - "browser": { - "./src/lib/fetch.js": "./src/lib/fetch-browser.js" - }, - "devDependencies": { - "standard": "^17.1.0", - "typescript": "^5.5.4" - }, - "overrides": { - "blake3-wasm@2.1.7": "^3.0.0", - "@c4312/blake3-internal": "^3.0.0" - } -} diff --git a/js/pubky/src/common/auth.js b/js/pubky/src/common/auth.js deleted file mode 100644 index f48d387..0000000 --- a/js/pubky/src/common/auth.js +++ /dev/null @@ -1,194 +0,0 @@ -import { Timestamp } from './timestamp.js' -import * as namespaces from './namespaces.js' -import * as crypto from './crypto.js' - -// 30 seconds -const TIME_INTERVAL = 30 * 1000000 - -export class AuthnSignature { - /** - * @param {number} time - * @param {import ('./crypto.js').KeyPair} signer - * @param {import ('./crypto.js').PublicKey} audience - * @param {Buffer} [token] - */ - constructor (time, signer, audience, token = crypto.randomBytes()) { - const timeStep = Math.floor(time / TIME_INTERVAL) - - const tokenHash = crypto.hash(token) - - const timeStepBytes = Buffer.allocUnsafe(8) - timeStepBytes.writeBigUint64BE(BigInt(timeStep)) - - const signature = signer.sign(signable( - signer.publicKey().bytes, - audience.bytes, - timeStepBytes, - tokenHash - )) - - this.bytes = Buffer.concat([ - signature, - tokenHash - ]) - } - - /** - * @param {import ('./crypto.js').KeyPair} signer - * @param {import ('./crypto.js').PublicKey} audience - * @param {Buffer} [token] - */ - static sign (signer, audience, token = crypto.randomBytes()) { - const time = Timestamp.now().microseconds - - return new AuthnSignature(time, signer, audience, token) - } - - asBytes () { - return new Uint8Array(this.bytes) - } -} - -export class AuthnVerifier { - #audience - /** @type {Array} */ - #seen - - /** - * @param {crypto.PublicKey} audience - */ - constructor (audience) { - this.#audience = audience - - this.#seen = [] - } - - #gc () { - const threshold = Timestamp.now().microseconds - const threshouldStep = Math.floor(threshold / TIME_INTERVAL) - 2 - - const thresholdBytes = Buffer.allocUnsafe(8) - thresholdBytes.writeBigUint64BE(BigInt(threshouldStep)) - - let count = 0 - - for (let i = 0; i < this.#seen.length; i++) { - if (this.#seen[i].subarray(0, 8).compare(thresholdBytes) > 0) { - break - } - count = i - } - - this.#seen.splice(0, count) - } - - /** - * @param {Buffer} bytes - * @param {crypto.PublicKey} signer - * - * @returns {true | Error} - */ - verify (bytes, signer) { - this.#gc() - - if (bytes.length !== 96) { - throw new Error(`InvalidLength: ${bytes.length}`) - } - - const signature = bytes.subarray(0, 64) - const tokenHash = bytes.subarray(64) - - const now = Timestamp.now().microseconds - const past = now - TIME_INTERVAL - const future = now + TIME_INTERVAL - - let result = verifyAt.call(this, now) - - if (!(result instanceof Error)) { - return result - } else if (result.toString() === 'Error: AuthnSignature already used') { - return result - } - - result = verifyAt.call(this, past) - - if (!(result instanceof Error)) { - return result - } else if (result.toString() === 'Error: AuthnSignature already used') { - return result - } - - return verifyAt.call(this, future) - - /** - * @param {number} time - */ - function verifyAt (time) { - const timeStep = Math.floor(time / TIME_INTERVAL) - - const timeStepBytes = Buffer.allocUnsafe(8) - timeStepBytes.writeBigUint64BE(BigInt(timeStep)) - - const result = signer.verify(signature, signable(signer.bytes, this.#audience.bytes, timeStepBytes, tokenHash)) - - const candidate = Buffer.concat([ - timeStepBytes, - tokenHash - ]) - - if (!(result instanceof Error)) { - const index = binarySearch(this.#seen, timeStepBytes) - - if (this.#seen[index]?.equals(candidate)) { - return new Error('AuthnSignature already used') - } - - this.#seen.splice(~index, 0, candidate) - - return - } - - return result - } - } -} - -/** - * @param {Array} arr - */ -function binarySearch (arr, element) { - let left = 0 - let right = arr.length - 1 - - while (left <= right) { - const mid = Math.floor((left + right) / 2) - - const comparison = arr[mid].subarray(0, 8).compare(element.subarray(0, 8)) - - if (comparison === 0) { - return mid - } else if (comparison < 0) { - left = mid + 1 - } else { - right = mid - 1 - } - } - - return left // Element not found, return the index where it should be inserted -} - -/** - * @param {Buffer} signer - * @param {Buffer} audience - * @param {Buffer} timeStepBytes - * @param {Buffer} tokenHash - */ -function signable (signer, audience, timeStepBytes, tokenHash) { - return Buffer.concat([ - namespaces.PUBKY_AUTHN, - timeStepBytes, - signer, - audience, - tokenHash - ]) -} diff --git a/js/pubky/src/common/crypto.js b/js/pubky/src/common/crypto.js deleted file mode 100644 index c1c81b0..0000000 --- a/js/pubky/src/common/crypto.js +++ /dev/null @@ -1,131 +0,0 @@ -//! Crypeo functions - -import sodium from 'sodium-universal' -import z32 from 'z32' - -// Blake3 - -/** @type {import('blake3-wasm')} */ -let loadedBlake3 - -const loadBlake3 = async () => { - if (loadedBlake3) return loadedBlake3 - // @ts-ignore - loadedBlake3 = await import('blake3-wasm').then(b3 => b3.load().then(() => b3)) - - return loadedBlake3 -} - -loadBlake3() - -/** - * It will return null if blake3 is not loaded yet! - * - * @param {Buffer} message - * - * @returns {Buffer | null} - */ -export const hash = (message) => { - return loadedBlake3?.createHash().update(message).digest() -} - -// Random -export const randomBytes = (n = 32) => { - const buf = Buffer.alloc(n) - sodium.randombytes_buf(buf) - return buf -} - -/// Keypairs - -/** - * @param {Buffer} buf - */ -export const zeroize = (buf) => { - buf.fill(0) -} - -export class KeyPair { - #publicKey - #secretKey - - /** - * @param {Buffer} seed - */ - constructor (seed) { - this.#publicKey = Buffer.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES) - this.#secretKey = Buffer.allocUnsafe(sodium.crypto_sign_SECRETKEYBYTES) - - if (seed) sodium.crypto_sign_seed_keypair(this.#publicKey, this.#secretKey, seed) - else sodium.crypto_sign_keypair(this.#publicKey, this.#secretKey) - } - - static random () { - const seed = randomBytes(32) - - return new KeyPair(seed) - } - - zeroize () { - zeroize(this.#secretKey) - this.secretKey = null - } - - publicKey () { - return new PublicKey(this.#publicKey) - } - - secretKey () { - return this.#secretKey - } - - /** - * @param {Uint8Array} message - */ - sign (message) { - const signature = Buffer.alloc(sodium.crypto_sign_BYTES) - sodium.crypto_sign_detached(signature, message, this.#secretKey) - - return signature - } -} - -export class PublicKey { - /** - * @param {Buffer} bytes - */ - constructor (bytes) { - this.bytes = bytes - } - - /** - * @param {string} string - * @returns {Error | PublicKey} - */ - static fromString (string) { - if (string.length !== 52) { - return new Error('Invalid PublicKey string, expected 52 characters, got: ' + string.length) - } - - try { - return new PublicKey(z32.decode(string)) - } catch (error) { - return error - } - } - - /** - * @param {Buffer} signature - * @param {Buffer} message - */ - verify (signature, message) { - const valid = sodium.crypto_sign_verify_detached(signature, message, this.bytes) - if (!valid) return new Error('Invalid signature') - - return true - } - - toString () { - return z32.encode(this.bytes) - } -} diff --git a/js/pubky/src/common/index.js b/js/pubky/src/common/index.js deleted file mode 100644 index 71ccb60..0000000 --- a/js/pubky/src/common/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export * as crypto from './crypto.js' -export { Timestamp } from './timestamp.js' -export { AuthnSignature, AuthnVerifier } from './auth.js' - -/** - * @typedef {string | number | boolean | null} JSONValue - * @typedef {{[key: string]: JSONValue | Array}} JSONObject - */ diff --git a/js/pubky/src/common/namespaces.js b/js/pubky/src/common/namespaces.js deleted file mode 100644 index 156d3af..0000000 --- a/js/pubky/src/common/namespaces.js +++ /dev/null @@ -1 +0,0 @@ -export const PUBKY_AUTHN = Buffer.from('PUBKY:AUTHN') diff --git a/js/pubky/src/common/timestamp.js b/js/pubky/src/common/timestamp.js deleted file mode 100644 index eeeadec..0000000 --- a/js/pubky/src/common/timestamp.js +++ /dev/null @@ -1,53 +0,0 @@ -import { CrockfordBase32 } from 'crockford-base32' -import { randomBytes } from './crypto.js' - -const clockId = randomBytes(1).readUintBE(0, 1) -let latest = 0 - -export class Timestamp { - /** - * @param {number} microseconds - u64 microseconds - */ - constructor (microseconds) { - /** microseconds as u64 */ - this.microseconds = microseconds - } - - static now () { - const now = Date.now() - latest = Math.max(now, latest + 1) - - return new Timestamp((latest * 1000) + clockId) - } - - /** - * @param {string} string - */ - static fromString (string) { - const microseconds = Number(CrockfordBase32.decode(string, { asNumber: true })) - return new Timestamp(microseconds) - } - - /** - * @param {Date} date - */ - static fromDate (date) { - const microseconds = Number(date) * 1000 - return new Timestamp(microseconds) - } - - toString () { - return CrockfordBase32.encode(this.microseconds) - } - - toDate () { - return new Date(this.microseconds / 1000) - } - - intoBytes () { - const buffer = Buffer.allocUnsafe(8) - buffer.writeBigUint64BE(BigInt(this.microseconds), 0) - - return buffer - } -} diff --git a/js/pubky/src/index.js b/js/pubky/src/index.js deleted file mode 100644 index e3e7548..0000000 --- a/js/pubky/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// import { PubkyClient as _PubkyClient } from './lib/client.js' -// import { PubkyError as _PubkyError } from './lib/error.js' -// -// export const PubkyClient = _PubkyClient; -// export const PubkyError = _PubkyError; diff --git a/js/pubky/src/lib/client.js b/js/pubky/src/lib/client.js deleted file mode 100644 index 742262d..0000000 --- a/js/pubky/src/lib/client.js +++ /dev/null @@ -1,102 +0,0 @@ -import Pkarr, { SignedPacket } from 'pkarr' -import { URL } from 'url' - -// import { AuthnSignature, crypto } from '@pubky/common' - -import * as crypto from '../common/crypto.js' - -// import { Pubky } from './pubky.js' -import fetch from './fetch.js' - - -const DEFAULT_PKARR_RELAY = new URL('https://relay.pkarr.org') - -export class PubkyClient { - // TODO: use DHT in nodejs - #pkarrRelay - - crypto = crypto - static crypto = crypto - - /** - * @param {object} [options={}] - * @param {URL} [options.pkarrRelay] - * - * @param {{relay: string, bootstrap: Array<{host: string, port: number}>}} [options.testnet] - */ - constructor(options = {}) { - this.#pkarrRelay = options.pkarrRelay || DEFAULT_PKARR_RELAY - } - - /** - * Publish the SVCB record for `_pubky.`. - * @param {crypto.KeyPair} keypair - * @param {String} host - */ - async publishPubkyHomeserver(keypair, host) { - - let existing = await (async () => { - try { - return (await Pkarr.relayGet(this.#pkarrRelay.toString(), keypair.publicKey().bytes)).packet() - } catch (error) { - return { - id: 0, - type: 'response', - flags: 0, - answers: [] - } - } - })(); - - let answers = [ - ]; - - for (let answer of existing.answers) { - if (!answer.name.startsWith("_pubky")) { - answers.push(answer) - } - } - - let signedPacket = SignedPacket.fromPacket(keypair, { - id: 0, - type: 'response', - flags: 0, - answers: [ - ...answers, - { - name: '_pubky.', type: 'SVCB', ttl: 7200, data: - - Buffer.from( - - ) - - } - ] - }) - - // let mut packet = Packet:: new_reply(0); - // - // if let Some(existing) = self.pkarr.resolve(& keypair.public_key()) ? { - // for answer in existing.packet().answers.iter().cloned() { - // if !answer.name.to_string().starts_with("_pubky") { - // packet.answers.push(answer.into_owned()) - // } - // } - // } - // - // let svcb = SVCB::new (0, host.try_into() ?); - // - // packet.answers.push(pkarr:: dns:: ResourceRecord:: new ( - // "_pubky".try_into().unwrap(), - // pkarr:: dns:: CLASS:: IN, - // 60 * 60, - // pkarr:: dns:: rdata:: RData:: SVCB(svcb), - // )); - // - // let signed_packet = SignedPacket:: from_packet(keypair, & packet) ?; - // - // self.pkarr.publish(& signed_packet) ?; - } -} - -export default PubkyClient diff --git a/js/pubky/src/lib/error.js b/js/pubky/src/lib/error.js deleted file mode 100644 index 29d4146..0000000 --- a/js/pubky/src/lib/error.js +++ /dev/null @@ -1 +0,0 @@ -export class PubkyError extends Error { } diff --git a/js/pubky/src/lib/fetch-browser.js b/js/pubky/src/lib/fetch-browser.js deleted file mode 100644 index 30b547c..0000000 --- a/js/pubky/src/lib/fetch-browser.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -const g = - (typeof globalThis !== 'undefined' && globalThis) || - // eslint-disable-next-line no-undef - (typeof self !== 'undefined' && self) || - // eslint-disable-next-line no-undef - (typeof global !== 'undefined' && global) || - {} - -// @ts-ignore -export default g.fetch diff --git a/js/pubky/src/lib/fetch.js b/js/pubky/src/lib/fetch.js deleted file mode 100644 index 7c24a8b..0000000 --- a/js/pubky/src/lib/fetch.js +++ /dev/null @@ -1,3 +0,0 @@ -import fetch from 'node-fetch-cache' - -export default fetch diff --git a/js/pubky/tsconfig.json b/js/pubky/tsconfig.json deleted file mode 100644 index 63d7a71..0000000 --- a/js/pubky/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - // Declarations control - "target": "esnext", - "module": "esnext", - - "noEmitOnError": true, - "emitDeclarationOnly": true, - "declarationMap": true, - "isolatedModules": true, - - "incremental": true, - "composite": true, - - // Check control - "strict": false, - "allowJs": true, - "checkJs": true, - - // module resolution - "esModuleInterop": true, - "moduleResolution": "node", - "resolveJsonModule": true, - - // advanced - "verbatimModuleSyntax": true, - "skipLibCheck": true, - - "outDir": "types" - }, - "include": ["src", "lib"] -} diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore index 7a5774d..bc0022f 100644 --- a/pubky/pkg/.gitignore +++ b/pubky/pkg/.gitignore @@ -1,2 +1,5 @@ nodejs/* -pubky.mjs +browser.js +coverage +node_modules +package-lock.json diff --git a/pubky/pkg/index.js b/pubky/pkg/index.js new file mode 100644 index 0000000..0af4fe0 --- /dev/null +++ b/pubky/pkg/index.js @@ -0,0 +1 @@ +export * from './nodejs/pubky.js' diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 6398be1..3c48852 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -8,12 +8,18 @@ "type": "git", "url": "https://github.com/pubky/pubky" }, + "scripts": { + "lint": "standard --fix", + "test": "brittle test/*.js -cov", + "prepublishOnly": "npm run lint && npm run test" + }, "files": [ "nodejs/*", - "pupky.mjs" + "index.js", + "browser.js" ], - "main": "nodejs/pubky.js", - "browser": "pubky.mjs", + "main": "index.js", + "browser": "browser.js", "types": "pubky.d.ts", "sideEffects": [ "./snippets/*" @@ -24,5 +30,9 @@ "dns", "decentralized", "identity" - ] + ], + "devDependencies": { + "brittle": "^3.6.1", + "standard": "^17.1.0" + } } diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js new file mode 100644 index 0000000..3564964 --- /dev/null +++ b/pubky/pkg/test/keys.js @@ -0,0 +1,13 @@ +import test from 'brittle' + +import { Keypair } from '../index.js' + +test('generate keys from a seed', async (t) => { + const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex') + + const keypair = Keypair.fromSecretKey(secretkey) + + const publicKey = keypair.publicKey() + + t.is(publicKey.toString(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') +}) diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs index 749b434..ebbf13b 100644 --- a/pubky/src/bin/patch.mjs +++ b/pubky/src/bin/patch.mjs @@ -56,4 +56,4 @@ const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `. `, ); -await writeFile(path.join(__dirname, `../../pkg/${name}.mjs`), patched); +await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched); diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 3859511..6f6c6dd 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -6,10 +6,40 @@ pub struct Keypair(pkarr::Keypair); #[wasm_bindgen] impl Keypair { #[wasm_bindgen] - pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { + /// Generate a random [Keypair] + pub fn random(secret_key: js_sys::Uint8Array) -> Self { + Self(pkarr::Keypair::random()) + } + + #[wasm_bindgen] + /// Generate a [Keypair] from a secret key. + pub fn fromSecretKey(secret_key: js_sys::Uint8Array) -> Self { let mut bytes = [0; 32]; secret_key.copy_to(&mut bytes); Self(pkarr::Keypair::from_secret_key(&bytes)) } + + #[wasm_bindgen] + /// Returns the [PublicKey] of this keypair. + pub fn publicKey(&self) -> PublicKey { + PublicKey(self.0.public_key()) + } +} + +#[wasm_bindgen] +pub struct PublicKey(pkarr::PublicKey); + +#[wasm_bindgen] +impl PublicKey { + #[wasm_bindgen] + /// Return the public key as Uint8Array + pub fn toBytes(&self) -> js_sys::Uint8Array { + js_sys::Uint8Array::from(self.0.as_bytes().as_slice()) + } + + #[wasm_bindgen] + pub fn toString(&self) -> String { + self.0.to_string() + } } From 42156b10f58f65a012a42da11956e25e3386fd69 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 20:10:50 +0300 Subject: [PATCH 011/125] feat(js): signup --- pubky/Cargo.toml | 2 +- pubky/pkg/package.json | 1 + pubky/pkg/test/auth.js | 45 +++++++++++++++++++++++++++++++++++++++ pubky/pkg/test/keys.js | 6 +++--- pubky/src/error.rs | 5 +++++ pubky/src/lib.rs | 5 +++-- pubky/src/wasm.rs | 4 ++-- pubky/src/wasm/client.rs | 2 +- pubky/src/wasm/keys.rs | 10 ++++----- pubky/src/wasm/pkarr.rs | 46 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 pubky/pkg/test/auth.js create mode 100644 pubky/src/wasm/pkarr.rs diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index e3f6e93..8396f3b 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -12,12 +12,12 @@ crate-type = ["cdylib", "rlib"] [dependencies] pkarr = "2.1.0" +thiserror = "1.0.62" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } ureq = { version = "2.10.0", features = ["cookies"] } -thiserror = "1.0.62" url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 3c48852..b3e43e9 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -11,6 +11,7 @@ "scripts": { "lint": "standard --fix", "test": "brittle test/*.js -cov", + "preinstall": "cargo run --bin bundle_pubky_npm", "prepublishOnly": "npm run lint && npm run test" }, "files": [ diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js new file mode 100644 index 0000000..0bc1b52 --- /dev/null +++ b/pubky/pkg/test/auth.js @@ -0,0 +1,45 @@ +import test from 'brittle' +import z32 from 'z32' + +import App from '@pubky/homeserver/test/helper/app.js' + +import Client from '../src/index.js' + +test('seed auth', async (t) => { + // const homeserver = await App(t) + + // const client = new Client( + // homeserver.homeserver.pkarr.serverPkarr.publicKey(), + // { + // relay: homeserver.testnet.relay + // } + // ) + // await client.ready() + // + // const seed = Client.crypto.generateSeed() + // const keypair = Client.crypto.generateKeyPair(seed) + // const expectedUserId = keypair.public_key().to_string() + // + // const userIdResult = await client.signup(seed) + // t.ok(userIdResult.isOk(), userIdResult.error) + // + // const userId = userIdResult.value + // t.is(userId, expectedUserId) + // + // const session = await client.session() + // t.ok(session?.users[userId]) + // + // { + // await client.logout(userId) + // + // const session = await client.session() + // t.absent(session?.users?.[userId]) + // } + // + // { + // await client.login(seed) + // + // const session = await client.session() + // t.ok(session?.users[userId]) + // } +}) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index 3564964..d01467a 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -5,9 +5,9 @@ import { Keypair } from '../index.js' test('generate keys from a seed', async (t) => { const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex') - const keypair = Keypair.fromSecretKey(secretkey) + const keypair = Keypair.from_secret_key(secretkey) - const publicKey = keypair.publicKey() + const publicKey = keypair.public_key() - t.is(publicKey.toString(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') + t.is(publicKey.to_string(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') }) diff --git a/pubky/src/error.rs b/pubky/src/error.rs index acbad4b..f051d14 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -23,18 +23,23 @@ pub enum Error { Pkarr(#[from] pkarr::Error), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Flume(#[from] flume::RecvError), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Ureq(#[from] Box), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Url(#[from] url::ParseError), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), } +#[cfg(not(target_arch = "wasm32"))] impl From for Error { fn from(error: ureq::Error) -> Self { Error::Ureq(Box::new(error)) diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index afa33b6..39955c4 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -14,13 +14,14 @@ macro_rules! if_wasm { )*} } +mod error; +pub use error::Error; + if_not_wasm! { mod client; mod client_async; - mod error; pub use client::PubkyClient; - pub use error::Error; } if_wasm! { diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index faddf5b..9610160 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,6 +1,6 @@ -mod keys; - mod client; +mod keys; +mod pkarr; pub use client::PubkyClient; pub use keys::Keypair; diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/client.rs index 550c8e5..e692cb8 100644 --- a/pubky/src/wasm/client.rs +++ b/pubky/src/wasm/client.rs @@ -11,7 +11,7 @@ pub struct Error {} #[wasm_bindgen] pub struct PubkyClient { - pkarr: PkarrRelayClient, + pub(crate) pkarr: PkarrRelayClient, } #[wasm_bindgen] diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 6f6c6dd..f73489d 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -13,7 +13,7 @@ impl Keypair { #[wasm_bindgen] /// Generate a [Keypair] from a secret key. - pub fn fromSecretKey(secret_key: js_sys::Uint8Array) -> Self { + pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { let mut bytes = [0; 32]; secret_key.copy_to(&mut bytes); @@ -22,7 +22,7 @@ impl Keypair { #[wasm_bindgen] /// Returns the [PublicKey] of this keypair. - pub fn publicKey(&self) -> PublicKey { + pub fn public_key(&self) -> PublicKey { PublicKey(self.0.public_key()) } } @@ -33,13 +33,13 @@ pub struct PublicKey(pkarr::PublicKey); #[wasm_bindgen] impl PublicKey { #[wasm_bindgen] - /// Return the public key as Uint8Array - pub fn toBytes(&self) -> js_sys::Uint8Array { + /// Convert the PublicKey to Uint8Array + pub fn to_uint8array(&self) -> js_sys::Uint8Array { js_sys::Uint8Array::from(self.0.as_bytes().as_slice()) } #[wasm_bindgen] - pub fn toString(&self) -> String { + pub fn to_string(&self) -> String { self.0.to_string() } } diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs new file mode 100644 index 0000000..81a5685 --- /dev/null +++ b/pubky/src/wasm/pkarr.rs @@ -0,0 +1,46 @@ +use wasm_bindgen::prelude::*; + +pub use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PublicKey, SignedPacket, +}; + +use crate::error::Result; + +use super::PubkyClient; + +// TODO: Share more code with the non-wasm client. + +impl PubkyClient { + /// Publish the SVCB record for `_pubky.`. + pub(crate) async fn publish_pubky_homeserver( + &self, + keypair: &Keypair, + host: &str, + ) -> Result<()> { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = self.pkarr.resolve(&keypair.public_key()).await? { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr.publish(&signed_packet).await?; + + Ok(()) + } +} From cdfd6c30ffb9940a02a38c8cdcde4d2aa5717a8b Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 21:10:19 +0300 Subject: [PATCH 012/125] refactor(pubky): refactor modules --- pubky/src/client.rs | 6 ++---- pubky/src/client/auth.rs | 5 ++--- pubky/src/client/public.rs | 4 +--- pubky/src/client_async.rs | 2 +- pubky/src/lib.rs | 13 ++++++++----- pubky/src/wasm.rs | 14 +++++++++----- pubky/src/wasm/{client.rs => auth.rs} | 10 +--------- pubky/src/wasm/pkarr.rs | 2 +- 8 files changed, 25 insertions(+), 31 deletions(-) rename pubky/src/wasm/{client.rs => auth.rs} (93%) diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 95742df..63fae96 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -7,10 +7,10 @@ use std::{collections::HashMap, fmt::format, time::Duration}; use ureq::{Agent, Response}; use url::Url; -use crate::error::{Error, Result}; - use pkarr::{DhtSettings, PkarrClient, Settings, Testnet}; +use crate::error::{Error, Result}; + #[derive(Debug, Clone)] pub struct PubkyClient { agent: Agent, @@ -40,8 +40,6 @@ impl PubkyClient { } } - // === Public Methods === - // === Private Methods === fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request { diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs index 8445640..a9f3da1 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/client/auth.rs @@ -1,9 +1,8 @@ -use crate::PubkyClient; +use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; -use super::{Error, HttpMethod, Result}; -use pkarr::{Keypair, PublicKey}; +use super::{Error, HttpMethod, PubkyClient, Result}; impl PubkyClient { /// Signup to a homeserver and update Pkarr accordingly. diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs index b54cd21..68b8209 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/client/public.rs @@ -2,9 +2,7 @@ use bytes::Bytes; use pkarr::PublicKey; -use crate::PubkyClient; - -use super::Result; +use super::{PubkyClient, Result}; impl PubkyClient { pub fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index 2fb7bd5..6fa063e 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use pkarr::{Keypair, PublicKey}; use pubky_common::session::Session; -use crate::{error::Result, PubkyClient}; +use crate::{client::PubkyClient, error::Result}; pub struct PubkyClientAsync(PubkyClient); diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 39955c4..87c535d 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -14,18 +14,21 @@ macro_rules! if_wasm { )*} } -mod error; -pub use error::Error; - if_not_wasm! { mod client; mod client_async; - pub use client::PubkyClient; + use pkarr::{PkarrClient}; + use ureq::{Agent, Response}; + use url::Url; } if_wasm! { mod wasm; - pub use wasm::{PubkyClient, Keypair}; + pub use wasm::keys::Keypair; + pub use wasm::PubkyClient; } + +mod error; +pub use error::Error; diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 9610160..a5589d3 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,6 +1,10 @@ -mod client; -mod keys; -mod pkarr; +use wasm_bindgen::prelude::*; -pub use client::PubkyClient; -pub use keys::Keypair; +pub mod auth; +pub mod keys; +pub mod pkarr; + +#[wasm_bindgen] +pub struct PubkyClient { + pub(crate) pkarr: pkarr::PkarrRelayClient, +} diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/auth.rs similarity index 93% rename from pubky/src/wasm/client.rs rename to pubky/src/wasm/auth.rs index e692cb8..e8bc842 100644 --- a/pubky/src/wasm/client.rs +++ b/pubky/src/wasm/auth.rs @@ -4,15 +4,7 @@ use web_sys::RequestMode; use pkarr::PkarrRelayClient; -use super::Keypair; - -#[wasm_bindgen] -pub struct Error {} - -#[wasm_bindgen] -pub struct PubkyClient { - pub(crate) pkarr: PkarrRelayClient, -} +use super::{keys::Keypair, PubkyClient}; #[wasm_bindgen] impl PubkyClient { diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 81a5685..97d39a1 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*; pub use pkarr::{ dns::{rdata::SVCB, Packet}, - Keypair, PublicKey, SignedPacket, + Keypair, PkarrRelayClient, PublicKey, SignedPacket, }; use crate::error::Result; From e05a49cd04c1a6bc7fdd4242c2930a4d9be4f142 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 21:27:18 +0300 Subject: [PATCH 013/125] fix: cargo clippy --- pubky-common/src/auth.rs | 4 ++-- pubky-common/src/session.rs | 7 ++++--- pubky/src/client/auth.rs | 2 +- pubky/src/lib.rs | 4 +--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 7fc2a02..5d5ebba 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -206,7 +206,7 @@ mod tests { let mut invalid = authn_signature.as_bytes().to_vec(); invalid[64..].copy_from_slice(&[0; 32]); - assert!(!verifier.verify(&invalid, &signer).is_ok()) + assert!(verifier.verify(&invalid, &signer).is_err()) } { @@ -214,7 +214,7 @@ mod tests { let mut invalid = authn_signature.as_bytes().to_vec(); invalid[0..32].copy_from_slice(&[0; 32]); - assert!(!verifier.verify(&invalid, &signer).is_ok()) + assert!(verifier.verify(&invalid, &signer).is_err()) } } } diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 9ef3c9d..5a35e14 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -69,9 +69,10 @@ mod tests { #[test] fn serialize() { - let mut session = Session::default(); - - session.user_agent = "foo".to_string(); + let session = Session { + user_agent: "foo".to_string(), + ..Default::default() + }; let serialized = session.serialize(); diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs index a9f3da1..b1f4cc8 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/client/auth.rs @@ -112,7 +112,7 @@ mod tests { match session { Err(Error::NotSignedIn) => {} - _ => assert!(false, "expected NotSignedInt error"), + _ => panic!("expected NotSignedInt error"), } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 87c535d..fab446f 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -18,9 +18,7 @@ if_not_wasm! { mod client; mod client_async; - use pkarr::{PkarrClient}; - use ureq::{Agent, Response}; - use url::Url; + use client::PubkyClient; } if_wasm! { From c466ca5546e9bfd33963527c6ff781cac648d6ef Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 27 Jul 2024 10:13:42 +0300 Subject: [PATCH 014/125] refactor(pubky): share helper functions between rust and wasm --- pubky/Cargo.toml | 3 +- pubky/src/client/pkarr.rs | 89 +++++++------------------------- pubky/src/error.rs | 7 ++- pubky/src/lib.rs | 2 + pubky/src/shared.rs | 1 + pubky/src/shared/pkarr.rs | 104 ++++++++++++++++++++++++++++++++++++++ pubky/src/wasm.rs | 10 ++++ pubky/src/wasm/auth.rs | 11 +--- pubky/src/wasm/keys.rs | 12 +++++ pubky/src/wasm/pkarr.rs | 28 ++-------- 10 files changed, 160 insertions(+), 107 deletions(-) create mode 100644 pubky/src/shared.rs create mode 100644 pubky/src/shared/pkarr.rs diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 8396f3b..8408c39 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -13,12 +13,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] pkarr = "2.1.0" thiserror = "1.0.62" +wasm-bindgen = "0.2.92" +url = "2.5.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } ureq = { version = "2.10.0", features = ["cookies"] } -url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" diff --git a/pubky/src/client/pkarr.rs b/pubky/src/client/pkarr.rs index c036527..a568c4c 100644 --- a/pubky/src/client/pkarr.rs +++ b/pubky/src/client/pkarr.rs @@ -4,33 +4,16 @@ pub use pkarr::{ Keypair, PkarrClient, PublicKey, Settings, SignedPacket, }; -use super::{Error, PubkyClient, Result, Url}; +use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; +use super::{Error, PubkyClient, Result, Url}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let mut packet = Packet::new_reply(0); + let existing = self.pkarr.resolve(&keypair.public_key())?; - if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let svcb = SVCB::new(0, host.try_into()?); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; self.pkarr.publish(&signed_packet)?; @@ -48,66 +31,32 @@ impl PubkyClient { /// Resolve a service's public_key and clearnet url from a Pubky domain pub(crate) fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { // TODO: cache the result of this function? - // TODO: use MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION - // TODO: move to common? let mut target = target.to_string(); let mut homeserver_public_key = None; let mut host = target.clone(); + let mut step = 0; + // PublicKey is very good at extracting the Pkarr TLD from a string. while let Ok(public_key) = PublicKey::try_from(target.clone()) { - if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { - let mut prior = None; + let response = self.pkarr.resolve(&public_key)?; - for answer in signed_packet.resource_records(&target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) - } - // TODO return random if priority is the same - } else { - prior = Some(svcb) - } - } - } + let done = parse_pubky_svcb( + response, + &public_key, + &mut target, + &mut homeserver_public_key, + &mut host, + &mut step, + ); - if let Some(svcb) = prior { - homeserver_public_key = Some(public_key); - target = svcb.target.to_string(); - - if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { - if port.len() < 2 { - // TODO: debug! Error encoding port! - } - let port = u16::from_be_bytes([port[0], port[1]]); - - host = format!("{target}:{port}"); - } else { - host.clone_from(&target); - }; - - continue; - } - }; - - break; + if done { + break; + } } - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; - - return Ok((homeserver, Url::parse(&url)?)); - } - - Err(Error::Generic("Could not resolve endpoint".to_string())) + format_url(homeserver_public_key, host) } } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index f051d14..2b5320e 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -22,6 +22,9 @@ pub enum Error { #[error(transparent)] Pkarr(#[from] pkarr::Error), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Flume(#[from] flume::RecvError), @@ -30,10 +33,6 @@ pub enum Error { #[cfg(not(target_arch = "wasm32"))] Ureq(#[from] Box), - #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Url(#[from] url::ParseError), - #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index fab446f..91a117d 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -29,4 +29,6 @@ if_wasm! { } mod error; +mod shared; + pub use error::Error; diff --git a/pubky/src/shared.rs b/pubky/src/shared.rs new file mode 100644 index 0000000..ad39b70 --- /dev/null +++ b/pubky/src/shared.rs @@ -0,0 +1 @@ +pub mod pkarr; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs new file mode 100644 index 0000000..e0d87cf --- /dev/null +++ b/pubky/src/shared/pkarr.rs @@ -0,0 +1,104 @@ +use url::Url; + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PublicKey, SignedPacket, +}; + +use crate::error::{Error, Result}; + +const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; + +pub fn prepare_packet_for_signup( + keypair: &Keypair, + host: &str, + existing: Option, +) -> Result { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = existing { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + Ok(SignedPacket::from_packet(keypair, &packet)?) +} + +pub fn parse_pubky_svcb( + signed_packet: Option, + public_key: &PublicKey, + target: &mut String, + homeserver_public_key: &mut Option, + host: &mut String, + step: &mut u8, +) -> bool { + *step += 1; + + let mut prior = None; + + if let Some(signed_packet) = signed_packet { + for answer in signed_packet.resource_records(target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } + } + } + + if let Some(svcb) = prior { + *homeserver_public_key = Some(public_key.clone()); + *target = svcb.target.to_string(); + + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + *host = format!("{target}:{port}"); + } else { + host.clone_from(target); + }; + + return *step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION; + } + } + + true +} + +pub fn format_url( + homeserver_public_key: Option, + host: String, +) -> Result<(PublicKey, Url)> { + if let Some(homeserver) = homeserver_public_key { + let url = if host.starts_with("localhost") { + format!("http://{host}") + } else { + format!("https://{host}") + }; + + return Ok((homeserver, Url::parse(&url)?)); + } + + Err(Error::Generic("Could not resolve endpoint".to_string())) +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index a5589d3..2414ba4 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -8,3 +8,13 @@ pub mod pkarr; pub struct PubkyClient { pub(crate) pkarr: pkarr::PkarrRelayClient, } + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + pkarr: pkarr::PkarrRelayClient::default(), + } + } +} diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index e8bc842..6f03834 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -8,19 +8,12 @@ use super::{keys::Keypair, PubkyClient}; #[wasm_bindgen] impl PubkyClient { - #[wasm_bindgen(constructor)] - pub fn new() -> Self { - Self { - pkarr: PkarrRelayClient::default(), - } - } - /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" #[wasm_bindgen] - pub fn signup(&self, secret_key: Keypair, homeserver: &str) -> Result<(), JsValue> { + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<(), JsError> { // let (audience, mut url) = self.resolve_endpoint(homeserver)?; // url.set_path(&format!("/{}", keypair.public_key())); @@ -29,7 +22,7 @@ impl PubkyClient { // fetch_base(url.to_string(), "PUT", body).await?; - // self.publish_pubky_homeserver(keypair, homeserver); + self.publish_pubky_homeserver(keypair, homeserver).await?; Ok(()) } diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index f73489d..65beae8 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -27,6 +27,12 @@ impl Keypair { } } +impl Keypair { + pub fn as_inner(&self) -> &pkarr::Keypair { + &self.0 + } +} + #[wasm_bindgen] pub struct PublicKey(pkarr::PublicKey); @@ -43,3 +49,9 @@ impl PublicKey { self.0.to_string() } } + +impl PublicKey { + pub fn as_inner(&self) -> &pkarr::PublicKey { + &self.0 + } +} diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 97d39a1..b000528 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -2,14 +2,13 @@ use wasm_bindgen::prelude::*; pub use pkarr::{ dns::{rdata::SVCB, Packet}, - Keypair, PkarrRelayClient, PublicKey, SignedPacket, + PkarrRelayClient, PublicKey, SignedPacket, }; use crate::error::Result; +use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -use super::PubkyClient; - -// TODO: Share more code with the non-wasm client. +use super::{keys::Keypair, PubkyClient}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -18,26 +17,9 @@ impl PubkyClient { keypair: &Keypair, host: &str, ) -> Result<()> { - let mut packet = Packet::new_reply(0); + let existing = self.pkarr.resolve(&keypair.public_key().as_inner()).await?; - if let Some(existing) = self.pkarr.resolve(&keypair.public_key()).await? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let svcb = SVCB::new(0, host.try_into()?); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + let signed_packet = prepare_packet_for_signup(keypair.as_inner(), host, existing)?; self.pkarr.publish(&signed_packet).await?; From ba50429b7ad46d8b79c6cc4f18a4536c2e971364 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 27 Jul 2024 18:31:34 +0300 Subject: [PATCH 015/125] feat(pubky): use reqwest and async instead of ureq --- Cargo.lock | 433 ++++++++++++++++++++++++++++++++++++- pubky/Cargo.toml | 1 + pubky/src/client.rs | 51 ++--- pubky/src/client/auth.rs | 62 +++--- pubky/src/client/pkarr.rs | 31 ++- pubky/src/client/public.rs | 2 +- pubky/src/client_async.rs | 94 -------- pubky/src/error.rs | 10 +- pubky/src/lib.rs | 1 - pubky/src/wasm.rs | 2 + pubky/src/wasm/auth.rs | 42 +--- 11 files changed, 509 insertions(+), 220 deletions(-) delete mode 100644 pubky/src/client_async.rs diff --git a/Cargo.lock b/Cargo.lock index b8b2d2d..dbb725b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -198,6 +204,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -305,9 +317,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" dependencies = [ "cookie", - "idna", + "idna 0.5.0", "indexmap", "log", + "publicsuffix", "serde", "serde_derive", "serde_json", @@ -315,6 +328,22 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -511,12 +540,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -551,6 +605,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -678,6 +747,25 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.2.1" @@ -737,7 +825,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bc30da4a93ff8cb98e535d595d6de42731d4719d707bc1c86f579158751a24e" dependencies = [ - "bitflags", + "bitflags 2.6.0", "byteorder", "heed-traits", "heed-types", @@ -830,6 +918,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -838,6 +927,40 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -847,12 +970,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -875,6 +1013,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itoa" version = "1.0.11" @@ -908,10 +1052,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "litrs" version = "0.4.1" @@ -1027,6 +1177,23 @@ dependencies = [ "getrandom", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1068,6 +1235,50 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1223,6 +1434,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "postcard" version = "1.0.8" @@ -1256,6 +1473,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "pubky" version = "0.1.0" @@ -1267,6 +1490,7 @@ dependencies = [ "pkarr", "pubky-common", "pubky_homeserver", + "reqwest", "thiserror", "tokio", "ureq", @@ -1316,6 +1540,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quote" version = "1.0.36" @@ -1361,7 +1595,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1419,6 +1653,51 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.17.8" @@ -1449,6 +1728,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.23.11" @@ -1464,6 +1756,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.7.0" @@ -1493,6 +1795,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1505,6 +1816,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.0.4" @@ -1650,7 +1984,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1747,6 +2081,39 @@ dependencies = [ "crossbeam-queue", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.62" @@ -1853,6 +2220,27 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -1905,7 +2293,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "http", "http-body", @@ -1990,6 +2378,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -2048,7 +2442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -2058,12 +2452,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2341,6 +2750,16 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "z32" version = "1.1.1" diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 8408c39..758af8e 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -15,6 +15,7 @@ pkarr = "2.1.0" thiserror = "1.0.62" wasm-bindgen = "0.2.92" url = "2.5.2" +reqwest = { version = "0.12.5", features = ["cookies"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 63fae96..2839110 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -1,33 +1,42 @@ mod auth; mod pkarr; -mod public; +// mod public; use std::{collections::HashMap, fmt::format, time::Duration}; -use ureq::{Agent, Response}; +use ::pkarr::PkarrClientAsync; use url::Url; use pkarr::{DhtSettings, PkarrClient, Settings, Testnet}; use crate::error::{Error, Result}; +static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + #[derive(Debug, Clone)] pub struct PubkyClient { - agent: Agent, - pkarr: PkarrClient, + http: reqwest::Client, + pkarr: PkarrClientAsync, } impl PubkyClient { pub fn new() -> Self { Self { - agent: Agent::new(), - pkarr: PkarrClient::new(Default::default()).unwrap(), + http: reqwest::Client::builder() + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(), + pkarr: PkarrClient::new(Default::default()).unwrap().as_async(), } } pub fn test(testnet: &Testnet) -> Self { Self { - agent: Agent::new(), + http: reqwest::Client::builder() + .cookie_store(true) + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(), pkarr: PkarrClient::new(Settings { dht: DhtSettings { request_timeout: Some(Duration::from_millis(10)), @@ -36,15 +45,10 @@ impl PubkyClient { }, ..Settings::default() }) - .unwrap(), + .unwrap() + .as_async(), } } - - // === Private Methods === - - fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request { - self.agent.request_url(method.into(), url) - } } impl Default for PubkyClient { @@ -52,22 +56,3 @@ impl Default for PubkyClient { Self::new() } } - -#[derive(Debug, Clone)] -pub enum HttpMethod { - Get, - Put, - Post, - Delete, -} - -impl From for &str { - fn from(value: HttpMethod) -> Self { - match value { - HttpMethod::Get => "GET", - HttpMethod::Put => "PUT", - HttpMethod::Post => "POST", - HttpMethod::Delete => "DELETE", - } - } -} diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs index b1f4cc8..c99a451 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/client/auth.rs @@ -1,23 +1,27 @@ -use pkarr::{Keypair, PublicKey}; +use reqwest::StatusCode; +use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; -use super::{Error, HttpMethod, PubkyClient, Result}; +use super::{Error, PubkyClient, Result}; impl PubkyClient { /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" - pub fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (audience, mut url) = self.resolve_endpoint(homeserver)?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { + let (audience, mut url) = self.resolve_endpoint(homeserver).await?; url.set_path(&format!("/{}", keypair.public_key())); - self.request(HttpMethod::Put, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())?; + let body = AuthnSignature::generate(keypair, &audience) + .as_bytes() + .to_owned(); - self.publish_pubky_homeserver(keypair, homeserver); + self.http.put(url).body(body).send().await?; + + self.publish_pubky_homeserver(keypair, homeserver).await?; Ok(()) } @@ -26,52 +30,50 @@ impl PubkyClient { /// /// Returns an [Error::NotSignedIn] if so, or [ureq::Error] if /// the response has any other `>=400` status code. - pub fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + pub async fn session(&self, pubky: &PublicKey) -> Result { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - let mut bytes = vec![]; + let res = self.http.get(url).send().await?; - let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new); + if res.status() == StatusCode::NOT_FOUND { + return Err(Error::NotSignedIn); + } - let reader = self.request(HttpMethod::Get, &url).call().map_err(|err| { - match err { - ureq::Error::Status(404, _) => Error::NotSignedIn, - // TODO: handle other types of errors - _ => err.into(), - } - })?; + if !res.status().is_success() { + res.error_for_status_ref()?; + }; - reader.into_reader().read_to_end(&mut bytes); + let bytes = res.bytes().await?; Ok(Session::deserialize(&bytes)?) } /// Signout from a homeserver. - pub fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - self.request(HttpMethod::Delete, &url) - .call() - .map_err(Box::new)?; + self.http.delete(url).send().await?; Ok(()) } /// Signin to a homeserver. - pub fn signin(&self, keypair: &Keypair) -> Result<()> { + pub async fn signin(&self, keypair: &Keypair) -> Result<()> { let pubky = keypair.public_key(); - let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?; + let (audience, mut url) = self.resolve_pubky_homeserver(&pubky).await?; url.set_path(&format!("/{}/session", &pubky)); - self.request(HttpMethod::Post, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) - .map_err(Box::new)?; + let body = AuthnSignature::generate(keypair, &audience) + .as_bytes() + .to_owned(); + + self.http.post(url).body(body).send().await?; Ok(()) } @@ -90,7 +92,7 @@ mod tests { let testnet = Testnet::new(3); let server = Homeserver::start_test(&testnet).await.unwrap(); - let client = PubkyClient::test(&testnet).as_async(); + let client = PubkyClient::test(&testnet); let keypair = Keypair::random(); diff --git a/pubky/src/client/pkarr.rs b/pubky/src/client/pkarr.rs index a568c4c..f22ae43 100644 --- a/pubky/src/client/pkarr.rs +++ b/pubky/src/client/pkarr.rs @@ -10,26 +10,34 @@ use super::{Error, PubkyClient, Result, Url}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. - pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let existing = self.pkarr.resolve(&keypair.public_key())?; + pub(crate) async fn publish_pubky_homeserver( + &self, + keypair: &Keypair, + host: &str, + ) -> Result<()> { + let existing = self.pkarr.resolve(&keypair.public_key()).await?; let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; - self.pkarr.publish(&signed_packet)?; + self.pkarr.publish(&signed_packet).await?; Ok(()) } /// Resolve the homeserver for a pubky. - pub(crate) fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, Url)> { + pub(crate) async fn resolve_pubky_homeserver( + &self, + pubky: &PublicKey, + ) -> Result<(PublicKey, Url)> { let target = format!("_pubky.{}", pubky); self.resolve_endpoint(&target) + .await .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) } /// Resolve a service's public_key and clearnet url from a Pubky domain - pub(crate) fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { // TODO: cache the result of this function? let mut target = target.to_string(); @@ -40,7 +48,7 @@ impl PubkyClient { // PublicKey is very good at extracting the Pkarr TLD from a string. while let Ok(public_key) = PublicKey::try_from(target.clone()) { - let response = self.pkarr.resolve(&public_key)?; + let response = self.pkarr.resolve(&public_key).await?; let done = parse_pubky_svcb( response, @@ -106,23 +114,24 @@ mod tests { pkarr_client.publish(&signed_packet).await.unwrap(); - tokio::task::spawn_blocking(move || { + { let client = PubkyClient::test(&testnet); let pubky = Keypair::random(); client - .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())) + .await + .unwrap(); let (public_key, url) = client .resolve_pubky_homeserver(&pubky.public_key()) + .await .unwrap(); assert_eq!(public_key, server.public_key()); assert_eq!(url.host_str(), Some("localhost")); assert_eq!(url.port(), Some(server.port())); - }) - .await - .expect("task failed") + } } } diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs index 68b8209..e2d396f 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/client/public.rs @@ -73,7 +73,7 @@ mod tests { let testnet = Testnet::new(3); let server = Homeserver::start_test(&testnet).await.unwrap(); - let client = PubkyClient::test(&testnet).as_async(); + let client = PubkyClient::test(&testnet); let keypair = Keypair::random(); diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs deleted file mode 100644 index 6fa063e..0000000 --- a/pubky/src/client_async.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::thread; - -use bytes::Bytes; - -use pkarr::{Keypair, PublicKey}; -use pubky_common::session::Session; - -use crate::{client::PubkyClient, error::Result}; - -pub struct PubkyClientAsync(PubkyClient); - -impl PubkyClient { - pub fn as_async(&self) -> PubkyClientAsync { - PubkyClientAsync(self.clone()) - } -} - -impl PubkyClientAsync { - /// Async version of [PubkyClient::signup] - pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let keypair = keypair.clone(); - let homeserver = homeserver.to_string(); - - thread::spawn(move || sender.send(client.signup(&keypair, &homeserver))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::session] - pub async fn session(&self, pubky: &PublicKey) -> Result { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - - thread::spawn(move || sender.send(client.session(&pubky))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::signout] - pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - - thread::spawn(move || sender.send(client.signout(&pubky))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::signin] - pub async fn signin(&self, keypair: &Keypair) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let keypair = keypair.clone(); - - thread::spawn(move || sender.send(client.signin(&keypair))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::put] - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - let path = path.to_string(); - let content = content.to_vec(); - - thread::spawn(move || sender.send(client.put(&pubky, &path, &content))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::get] - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - let path = path.to_string(); - - thread::spawn(move || sender.send(client.get(&pubky, &path))); - - receiver.recv_async().await? - } -} diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 2b5320e..398cb7b 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -30,17 +30,9 @@ pub enum Error { Flume(#[from] flume::RecvError), #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Ureq(#[from] Box), + Reqwest(#[from] reqwest::Error), #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), } - -#[cfg(not(target_arch = "wasm32"))] -impl From for Error { - fn from(error: ureq::Error) -> Self { - Error::Ureq(Box::new(error)) - } -} diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 91a117d..36ff2c0 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -16,7 +16,6 @@ macro_rules! if_wasm { if_not_wasm! { mod client; - mod client_async; use client::PubkyClient; } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 2414ba4..a8d438f 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -6,6 +6,7 @@ pub mod pkarr; #[wasm_bindgen] pub struct PubkyClient { + pub(crate) http: reqwest::Client, pub(crate) pkarr: pkarr::PkarrRelayClient, } @@ -14,6 +15,7 @@ impl PubkyClient { #[wasm_bindgen(constructor)] pub fn new() -> Self { Self { + http: reqwest::Client::new(), pkarr: pkarr::PkarrRelayClient::default(), } } diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index 6f03834..08b802d 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -13,50 +13,24 @@ impl PubkyClient { /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" #[wasm_bindgen] - pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<(), JsError> { - // let (audience, mut url) = self.resolve_endpoint(homeserver)?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result { + let (audience, mut url) = self.resolve_endpoint(homeserver)?; - // url.set_path(&format!("/{}", keypair.public_key())); + url.set_path(&format!("/{}", keypair.public_key())); - // let body = AuthnSignature::generate(keypair, &audience).as_bytes(); + self.http + .put(&url) + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())?; - // fetch_base(url.to_string(), "PUT", body).await?; - - self.publish_pubky_homeserver(keypair, homeserver).await?; + self.publish_pubky_homeserver(keypair, homeserver).await; Ok(()) } } -async fn fetch_base( - url: &String, - method: &str, - body: Option>, -) -> Result { - let mut opts = web_sys::RequestInit::new(); - opts.method(method); - opts.mode(RequestMode::Cors); - - if let Some(body) = body { - let body_bytes: &[u8] = &body; - let body_array: js_sys::Uint8Array = body_bytes.into(); - let js_value: &JsValue = body_array.as_ref(); - opts.body(Some(js_value)); - } - - let js_request = web_sys::Request::new_with_str_and_init(url, &opts)?; - - let window = web_sys::window().unwrap(); - let response = JsFuture::from(window.fetch_with_request(&js_request)).await?; - - let response: web_sys::Response = response.dyn_into()?; - - Ok(response) -} - #[cfg(test)] mod tests { - use wasm_bindgen_test::*; + use wasm_bindgen_test::wasm_bindgen_test; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); From c40ba8390be1e4bec2f474a4caf5007c4daba3cb Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 27 Jul 2024 19:31:26 +0300 Subject: [PATCH 016/125] feat(pubky): enable get and put methods --- pubky/src/client.rs | 2 +- pubky/src/client/public.rs | 32 ++++++++------------------------ 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 2839110..d114a72 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -1,6 +1,6 @@ mod auth; mod pkarr; -// mod public; +mod public; use std::{collections::HashMap, fmt::format, time::Duration}; diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs index e2d396f..2c1b39f 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/client/public.rs @@ -5,41 +5,31 @@ use pkarr::PublicKey; use super::{PubkyClient, Result}; impl PubkyClient { - pub fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { let path = normalize_path(path); - let (_, mut url) = self.resolve_pubky_homeserver(pubky)?; + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{pubky}/{path}")); - self.request(super::HttpMethod::Put, &url) - .send_bytes(content)?; + self.http.put(url).body(content.to_owned()).send().await?; Ok(()) } - pub fn get(&self, pubky: &PublicKey, path: &str) -> Result { + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { let path = normalize_path(path); - let (_, mut url) = self.resolve_pubky_homeserver(pubky)?; + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{pubky}/{path}")); - let response = self.request(super::HttpMethod::Get, &url).call()?; - - let len = response - .header("Content-Length") - .and_then(|s| s.parse::().ok()) - // TODO: return an error in case content-length header is missing - .unwrap_or(0); + let response = self.http.get(url).send().await?; // TODO: bail on too large files. + let bytes = response.bytes().await?; - let mut bytes = vec![0; len as usize]; - - response.into_reader().read_exact(&mut bytes); - - Ok(bytes.into()) + Ok(bytes) } } @@ -86,12 +76,6 @@ mod tests { .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) .await; - if let Err(Error::Ureq(ureqerror)) = response { - if let Some(r) = ureqerror.into_response() { - dbg!(r.into_string()); - } - } - let response = client .get(&keypair.public_key(), "/pub/foo.txt") .await From dac22840658d217534a86e01b40957c3d5b5e92f Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 12:22:20 +0300 Subject: [PATCH 017/125] feat(homeserver): add in memory pkarr relay api for testing --- Cargo.lock | 121 +++++++++++++++++++++++++++ pubky-homeserver/Cargo.toml | 2 + pubky-homeserver/src/main.rs | 27 +++++- pubky-homeserver/src/routes.rs | 9 +- pubky-homeserver/src/routes/pkarr.rs | 69 +++++++++++++++ pubky-homeserver/src/server.rs | 2 + 6 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 pubky-homeserver/src/routes/pkarr.rs diff --git a/Cargo.lock b/Cargo.lock index dbb725b..61051c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -271,12 +320,58 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "cobs" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -819,6 +914,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "heed" version = "0.20.3" @@ -1019,6 +1120,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -1525,10 +1632,12 @@ dependencies = [ "axum-extra", "base32", "bytes", + "clap", "dirs-next", "flume", "futures-util", "heed", + "once_cell", "pkarr", "postcard", "pubky-common", @@ -2043,6 +2152,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2446,6 +2561,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index da0c5c7..68e30f0 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -9,10 +9,12 @@ axum = "0.7.5" axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] } base32 = "0.5.1" bytes = "1.6.1" +clap = { version = "4.5.11", features = ["derive"] } dirs-next = "2.0.0" flume = "0.11.0" futures-util = "0.3.30" heed = "0.20.3" +once_cell = "1.19.0" pkarr = { version = "2.1.0", features = ["async"] } postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } diff --git a/pubky-homeserver/src/main.rs b/pubky-homeserver/src/main.rs index a54fba4..0d22194 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -1,13 +1,36 @@ use anyhow::Result; +use pkarr::mainline::Testnet; use pubky_homeserver::Homeserver; +use clap::Parser; + +#[derive(Parser, Debug)] +struct Cli { + /// [tracing_subscriber::EnvFilter] + #[clap(short, long)] + tracing_env_filter: Option, + #[clap(long)] + testnet: bool, +} + #[tokio::main] async fn main() -> Result<()> { + let args = Cli::parse(); + tracing_subscriber::fmt() - .with_env_filter("pubky_homeserver=debug,tower_http=debug") + .with_env_filter( + args.tracing_env_filter + .unwrap_or("pubky_homeserver=debug,tower_http=debug".to_string()), + ) .init(); - let server = Homeserver::start(Default::default()).await?; + let server = if args.testnet { + let testnet = Testnet::new(3); + + Homeserver::start_test(&testnet).await? + } else { + Homeserver::start(Default::default()).await? + }; server.run_until_done().await?; diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 3b872c1..e1a4fef 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -8,11 +8,14 @@ use tower_http::trace::TraceLayer; use crate::server::AppState; +use self::pkarr::pkarr_router; + mod auth; +mod pkarr; mod public; mod root; -pub fn create_app(state: AppState) -> Router { +fn base(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) .route("/:pubky", put(auth::signup)) @@ -28,3 +31,7 @@ pub fn create_app(state: AppState) -> Router { .layer(DefaultBodyLimit::max(16 * 1024)) .with_state(state) } + +pub fn create_app(state: AppState) -> Router { + base(state).merge(pkarr_router()) +} diff --git a/pubky-homeserver/src/routes/pkarr.rs b/pubky-homeserver/src/routes/pkarr.rs new file mode 100644 index 0000000..9c9253b --- /dev/null +++ b/pubky-homeserver/src/routes/pkarr.rs @@ -0,0 +1,69 @@ +use std::{collections::HashMap, sync::RwLock}; + +use axum::{ + body::{Body, Bytes}, + http::StatusCode, + response::IntoResponse, + routing::{get, put}, + Router, +}; +use futures_util::stream::StreamExt; +use once_cell::sync::OnceCell; + +use pkarr::{PublicKey, SignedPacket}; + +use crate::{ + error::{Error, Result}, + extractors::Pubky, +}; + +// TODO: maybe replace after we have local storage of users packets? +static IN_MEMORY: OnceCell>> = OnceCell::new(); + +/// Pkarr relay, helpful for testing. +/// +/// For real productioin, you should use a [production ready +/// relay](https://github.com/pubky/pkarr/server). +pub fn pkarr_router() -> Router { + Router::new() + .route("/pkarr/:pubky", put(pkarr_put)) + .route("/pkarr/:pubky", get(pkarr_get)) +} + +pub async fn pkarr_put(pubky: Pubky, body: Body) -> Result { + let mut bytes = Vec::with_capacity(1104); + + let mut stream = body.into_data_stream(); + + while let Some(chunk) = stream.next().await { + bytes.extend_from_slice(&chunk?) + } + + let public_key = pubky.public_key().to_owned(); + + let signed_packet = SignedPacket::from_relay_payload(&public_key, &Bytes::from(bytes))?; + + let mut store = IN_MEMORY + .get() + .expect("In memory pkarr store is not initialized") + .write() + .unwrap(); + + store.insert(public_key, signed_packet); + + Ok(()) +} + +pub async fn pkarr_get(pubky: Pubky) -> Result { + let store = IN_MEMORY + .get() + .expect("In memory pkarr store is not initialized") + .read() + .unwrap(); + + if let Some(signed_packet) = store.get(pubky.public_key()) { + return Ok(signed_packet.to_relay_payload()); + } + + Err(Error::with_status(StatusCode::NOT_FOUND)) +} diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 0a2f3ae..7d37baf 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -82,6 +82,8 @@ impl Homeserver { /// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage. pub async fn start_test(testnet: &Testnet) -> Result { + info!("Running testnet.."); + Homeserver::start(Config::test(testnet)).await } From d35c586a1269040ef6ad2898707d070dfd497be7 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 18:38:21 +0300 Subject: [PATCH 018/125] feat(pubky): start testing js package against local homeserver --- Cargo.lock | 52 +------------ pubky-common/Cargo.toml | 3 + pubky-common/src/timestamp.rs | 13 +++- pubky-homeserver/Cargo.toml | 1 - pubky-homeserver/src/config.rs | 17 +++-- pubky-homeserver/src/main.rs | 11 ++- pubky-homeserver/src/routes.rs | 19 ++++- pubky-homeserver/src/routes/pkarr.rs | 38 ++++----- pubky-homeserver/src/server.rs | 30 +++++--- pubky/Cargo.toml | 16 ++-- pubky/pkg/test/auth.js | 13 ++-- pubky/src/bin/bundle_pubky_npm.rs | 2 +- pubky/src/error.rs | 14 ++++ pubky/src/lib.rs | 45 +++++------ pubky/src/{client.rs => native.rs} | 26 +++---- pubky/src/{client => native}/auth.rs | 18 +++-- pubky/src/{client => native}/pkarr.rs | 30 ++++++-- pubky/src/{client => native}/public.rs | 7 +- pubky/src/shared/pkarr.rs | 3 +- pubky/src/wasm.rs | 8 +- pubky/src/wasm/auth.rs | 23 ++++-- pubky/src/wasm/keys.rs | 13 ++++ pubky/src/wasm/pkarr.rs | 102 +++++++++++++++++++++++-- 23 files changed, 309 insertions(+), 195 deletions(-) rename pubky/src/{client.rs => native.rs} (76%) rename pubky/src/{client => native}/auth.rs (88%) rename pubky/src/{client => native}/pkarr.rs (85%) rename pubky/src/{client => native}/public.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index 61051c5..8d6ec73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,6 @@ checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" dependencies = [ "cookie", "idna 0.5.0", - "indexmap", "log", "publicsuffix", "serde", @@ -463,15 +462,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - [[package]] name = "critical-section" version = "1.1.2" @@ -672,16 +662,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "flate2" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.0" @@ -1600,7 +1580,6 @@ dependencies = [ "reqwest", "thiserror", "tokio", - "ureq", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1615,6 +1594,7 @@ dependencies = [ "base32", "blake3", "ed25519-dalek", + "js-sys", "once_cell", "pkarr", "postcard", @@ -1637,7 +1617,6 @@ dependencies = [ "flume", "futures-util", "heed", - "once_cell", "pkarr", "postcard", "pubky-common", @@ -1856,9 +1835,7 @@ version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ - "log", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2532,24 +2509,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" -dependencies = [ - "base64 0.22.1", - "cookie", - "cookie_store", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots", -] - [[package]] name = "url" version = "2.5.2" @@ -2701,15 +2660,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 1b7111c..6855f97 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -15,3 +15,6 @@ rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } serde = { version = "1.0.204", features = ["derive"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3.69" diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index f850661..4c546d5 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -1,7 +1,6 @@ //! Monotonic unix timestamp in microseconds use std::fmt::Display; -use std::time::SystemTime; use std::{ ops::{Add, Sub}, sync::Mutex, @@ -10,6 +9,9 @@ use std::{ use once_cell::sync::Lazy; use rand::Rng; +#[cfg(not(target_arch = "wasm32"))] +use std::time::SystemTime; + /// ~4% chance of none of 10 clocks have matching id. const CLOCK_MASK: u64 = (1 << 8) - 1; const TIME_MASK: u64 = !0 >> 8; @@ -162,6 +164,15 @@ fn system_time() -> u64 { .as_micros() as u64 } +#[cfg(target_arch = "wasm32")] +/// Return the number of microseconds since [SystemTime::UNIX_EPOCH] +pub fn system_time() -> u64 { + // Won't be an issue for more than 5000 years! + (js_sys::Date::now() as u64 ) + // Turn miliseconds to microseconds + * 1000 +} + #[derive(thiserror::Error, Debug)] pub enum TimestampError { #[error("Invalid bytes length, Timestamp should be encoded as 8 bytes, got {0}")] diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 68e30f0..698a3e6 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -14,7 +14,6 @@ dirs-next = "2.0.0" flume = "0.11.0" futures-util = "0.3.30" heed = "0.20.3" -once_cell = "1.19.0" pkarr = { version = "2.1.0", features = ["async"] } postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 3657ecd..6949b09 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -1,9 +1,9 @@ //! Configuration for the server use anyhow::{anyhow, Result}; -use pkarr::Keypair; +use pkarr::{mainline::dht::DhtSettings, Keypair}; // use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, path::PathBuf}; +use std::{fmt::Debug, path::PathBuf, time::Duration}; use pubky_common::timestamp::Timestamp; @@ -18,14 +18,15 @@ const DEFAULT_STORAGE_DIR: &str = "pubky"; Clone, )] pub struct Config { - port: Option, - bootstrap: Option>, - domain: String, + pub port: Option, + pub bootstrap: Option>, + pub domain: String, /// Path to the storage directory /// /// Defaults to a directory in the OS data directory - storage: Option, - keypair: Keypair, + pub storage: Option, + pub keypair: Keypair, + pub request_timeout: Option, } impl Config { @@ -50,6 +51,7 @@ impl Config { Self { bootstrap, storage, + request_timeout: Some(Duration::from_millis(10)), ..Default::default() } } @@ -93,6 +95,7 @@ impl Default for Config { domain: "localhost".to_string(), storage: None, keypair: Keypair::random(), + request_timeout: None, } } } diff --git a/pubky-homeserver/src/main.rs b/pubky-homeserver/src/main.rs index 0d22194..77b3382 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use pkarr::mainline::Testnet; -use pubky_homeserver::Homeserver; +use pkarr::{mainline::Testnet, Keypair}; +use pubky_homeserver::{config::Config, Homeserver}; use clap::Parser; @@ -27,7 +27,12 @@ async fn main() -> Result<()> { let server = if args.testnet { let testnet = Testnet::new(3); - Homeserver::start_test(&testnet).await? + Homeserver::start(Config { + port: Some(15411), + keypair: Keypair::from_secret_key(&[0_u8; 32]), + ..Config::test(&testnet) + }) + .await? } else { Homeserver::start(Default::default()).await? }; diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index e1a4fef..3f53d9b 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -1,10 +1,16 @@ +use std::sync::Arc; + use axum::{ extract::DefaultBodyLimit, + http::Method, routing::{delete, get, post, put}, Router, }; use tower_cookies::CookieManagerLayer; -use tower_http::trace::TraceLayer; +use tower_http::{ + cors::{self, CorsLayer}, + trace::TraceLayer, +}; use crate::server::AppState; @@ -24,7 +30,6 @@ fn base(state: AppState) -> Router { .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*path", put(public::put)) .route("/:pubky/*path", get(public::get)) - .layer(TraceLayer::new_for_http()) .layer(CookieManagerLayer::new()) // TODO: revisit if we enable streaming big payloads // TODO: maybe add to a separate router (drive router?). @@ -33,5 +38,13 @@ fn base(state: AppState) -> Router { } pub fn create_app(state: AppState) -> Router { - base(state).merge(pkarr_router()) + base(state.clone()) + // TODO: Only enable this for test environments? + .nest("/pkarr", pkarr_router(state)) + .layer( + CorsLayer::new() + .allow_methods([Method::GET, Method::PUT, Method::POST, Method::DELETE]) + .allow_origin(cors::Any), + ) + .layer(TraceLayer::new_for_http()) } diff --git a/pubky-homeserver/src/routes/pkarr.rs b/pubky-homeserver/src/routes/pkarr.rs index 9c9253b..977a129 100644 --- a/pubky-homeserver/src/routes/pkarr.rs +++ b/pubky-homeserver/src/routes/pkarr.rs @@ -2,35 +2,39 @@ use std::{collections::HashMap, sync::RwLock}; use axum::{ body::{Body, Bytes}, + extract::State, http::StatusCode, response::IntoResponse, routing::{get, put}, Router, }; use futures_util::stream::StreamExt; -use once_cell::sync::OnceCell; use pkarr::{PublicKey, SignedPacket}; +use tracing::debug; use crate::{ error::{Error, Result}, extractors::Pubky, + server::AppState, }; -// TODO: maybe replace after we have local storage of users packets? -static IN_MEMORY: OnceCell>> = OnceCell::new(); - /// Pkarr relay, helpful for testing. /// /// For real productioin, you should use a [production ready /// relay](https://github.com/pubky/pkarr/server). -pub fn pkarr_router() -> Router { +pub fn pkarr_router(state: AppState) -> Router { Router::new() - .route("/pkarr/:pubky", put(pkarr_put)) - .route("/pkarr/:pubky", get(pkarr_get)) + .route("/:pubky", put(pkarr_put)) + .route("/:pubky", get(pkarr_get)) + .with_state(state) } -pub async fn pkarr_put(pubky: Pubky, body: Body) -> Result { +pub async fn pkarr_put( + State(mut state): State, + pubky: Pubky, + body: Body, +) -> Result { let mut bytes = Vec::with_capacity(1104); let mut stream = body.into_data_stream(); @@ -43,25 +47,13 @@ pub async fn pkarr_put(pubky: Pubky, body: Body) -> Result { let signed_packet = SignedPacket::from_relay_payload(&public_key, &Bytes::from(bytes))?; - let mut store = IN_MEMORY - .get() - .expect("In memory pkarr store is not initialized") - .write() - .unwrap(); - - store.insert(public_key, signed_packet); + state.pkarr_client.publish(&signed_packet).await?; Ok(()) } -pub async fn pkarr_get(pubky: Pubky) -> Result { - let store = IN_MEMORY - .get() - .expect("In memory pkarr store is not initialized") - .read() - .unwrap(); - - if let Some(signed_packet) = store.get(pubky.public_key()) { +pub async fn pkarr_get(State(state): State, pubky: Pubky) -> Result { + if let Some(signed_packet) = state.pkarr_client.resolve(pubky.public_key()).await? { return Ok(signed_packet.to_relay_payload()); } diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 7d37baf..f943203 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,13 +1,16 @@ -use std::{future::IntoFuture, net::SocketAddr}; +use std::{ + collections::HashMap, future::IntoFuture, net::SocketAddr, num::NonZeroUsize, sync::Arc, +}; use anyhow::{Error, Result}; +use lru::LruCache; use pubky_common::auth::AuthnVerifier; -use tokio::{net::TcpListener, signal, task::JoinSet}; +use tokio::{net::TcpListener, signal, sync::Mutex, task::JoinSet}; use tracing::{debug, info, warn}; use pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PublicKey, Settings, + PkarrClient, PkarrClientAsync, PublicKey, Settings, SignedPacket, }; use crate::{config::Config, database::DB, pkarr::publish_server_packet}; @@ -23,6 +26,7 @@ pub struct Homeserver { pub(crate) struct AppState { pub verifier: AuthnVerifier, pub db: DB, + pub pkarr_client: PkarrClientAsync, } impl Homeserver { @@ -33,9 +37,20 @@ impl Homeserver { let db = DB::open(&config.storage()?)?; + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: config.bootstsrap(), + request_timeout: config.request_timeout, + ..Default::default() + }, + ..Default::default() + })? + .as_async(); + let state = AppState { verifier: AuthnVerifier::new(public_key.clone()), db, + pkarr_client: pkarr_client.clone(), }; let app = crate::routes::create_app(state); @@ -60,15 +75,6 @@ impl Homeserver { info!("Homeserver listening on http://localhost:{port}"); - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: config.bootstsrap(), - ..Default::default() - }, - ..Default::default() - })? - .as_async(); - publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?; info!("Homeserver listening on pubky://{public_key}"); diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 758af8e..34e1d5c 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -11,20 +11,22 @@ keywords = ["web", "dht", "dns", "decentralized", "identity"] crate-type = ["cdylib", "rlib"] [dependencies] -pkarr = "2.1.0" thiserror = "1.0.62" wasm-bindgen = "0.2.92" url = "2.5.2" reqwest = { version = "0.12.5", features = ["cookies"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -pubky-common = { version = "0.1.0", path = "../pubky-common" } - -ureq = { version = "2.10.0", features = ["cookies"] } -flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" +pubky-common = { version = "0.1.0", path = "../pubky-common" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +pkarr = { version="2.1.0", features = ["async"] } + +flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } + [target.'cfg(target_arch = "wasm32")'.dependencies] +pkarr = { version = "2.1.0", default-features = false } + futures = "0.3.29" js-sys = "0.3.69" wasm-bindgen = "0.2.92" diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 0bc1b52..23cfe8f 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -1,12 +1,15 @@ import test from 'brittle' -import z32 from 'z32' -import App from '@pubky/homeserver/test/helper/app.js' - -import Client from '../src/index.js' +import { PubkyClient, Keypair, PublicKey } from '../index.js' test('seed auth', async (t) => { - // const homeserver = await App(t) + + let client = new PubkyClient(); + + let keypair = Keypair.random(); + let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); + + await client.signup(keypair, homeserver); // const client = new Client( // homeserver.homeserver.pkarr.serverPkarr.publicKey(), diff --git a/pubky/src/bin/bundle_pubky_npm.rs b/pubky/src/bin/bundle_pubky_npm.rs index b3305d3..40e9b90 100644 --- a/pubky/src/bin/bundle_pubky_npm.rs +++ b/pubky/src/bin/bundle_pubky_npm.rs @@ -5,7 +5,7 @@ use std::process::{Command, ExitStatus}; // If the process hangs, try `cargo clean` to remove all locks. fn main() { - println!("cargo:rerun-if-changed=client/"); + println!("Building wasm for pubky..."); build_wasm("nodejs").unwrap(); patch().unwrap(); diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 398cb7b..18be027 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -35,4 +35,18 @@ pub enum Error { #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), + + #[error("Could not resolve endpoint for {0}")] + ResolveEndpoint(String), +} + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +#[cfg(target_arch = "wasm32")] +impl From for JsValue { + fn from(error: Error) -> JsValue { + let error_message = error.to_string(); + js_sys::Error::new(&error_message).into() + } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 36ff2c0..1c35ff8 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -1,33 +1,24 @@ #![allow(unused)] -macro_rules! if_not_wasm { - ($($item:item)*) => {$( - #[cfg(not(target_arch = "wasm32"))] - $item - )*} -} - -macro_rules! if_wasm { - ($($item:item)*) => {$( - #[cfg(target_arch = "wasm32")] - $item - )*} -} - -if_not_wasm! { - mod client; - - use client::PubkyClient; -} - -if_wasm! { - mod wasm; - - pub use wasm::keys::Keypair; - pub use wasm::PubkyClient; -} - mod error; mod shared; +#[cfg(not(target_arch = "wasm32"))] +mod native; + +#[cfg(target_arch = "wasm32")] +mod wasm; +use wasm_bindgen::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +use ::pkarr::PkarrClientAsync; + pub use error::Error; + +#[derive(Debug, Clone)] +#[wasm_bindgen] +pub struct PubkyClient { + http: reqwest::Client, + #[cfg(not(target_arch = "wasm32"))] + pkarr: PkarrClientAsync, +} diff --git a/pubky/src/client.rs b/pubky/src/native.rs similarity index 76% rename from pubky/src/client.rs rename to pubky/src/native.rs index d114a72..90a1930 100644 --- a/pubky/src/client.rs +++ b/pubky/src/native.rs @@ -1,24 +1,18 @@ -mod auth; -mod pkarr; -mod public; +pub mod auth; +pub mod pkarr; +pub mod public; -use std::{collections::HashMap, fmt::format, time::Duration}; +use std::time::Duration; -use ::pkarr::PkarrClientAsync; -use url::Url; +use ::pkarr::{ + mainline::dht::{DhtSettings, Testnet}, + PkarrClient, PkarrClientAsync, Settings, +}; -use pkarr::{DhtSettings, PkarrClient, Settings, Testnet}; - -use crate::error::{Error, Result}; +use crate::PubkyClient; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); -#[derive(Debug, Clone)] -pub struct PubkyClient { - http: reqwest::Client, - pkarr: PkarrClientAsync, -} - impl PubkyClient { pub fn new() -> Self { Self { @@ -26,10 +20,12 @@ impl PubkyClient { .user_agent(DEFAULT_USER_AGENT) .build() .unwrap(), + #[cfg(not(target_arch = "wasm32"))] pkarr: PkarrClient::new(Default::default()).unwrap().as_async(), } } + #[cfg(not(target_arch = "wasm32"))] pub fn test(testnet: &Testnet) -> Self { Self { http: reqwest::Client::builder() diff --git a/pubky/src/client/auth.rs b/pubky/src/native/auth.rs similarity index 88% rename from pubky/src/client/auth.rs rename to pubky/src/native/auth.rs index c99a451..76ef81e 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/native/auth.rs @@ -3,15 +3,20 @@ use reqwest::StatusCode; use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; -use super::{Error, PubkyClient, Result}; +use crate::{ + error::{Error, Result}, + PubkyClient, +}; impl PubkyClient { /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" - pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (audience, mut url) = self.resolve_endpoint(homeserver).await?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<()> { + let homeserver = homeserver.to_string(); + + let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; url.set_path(&format!("/{}", keypair.public_key())); @@ -21,7 +26,7 @@ impl PubkyClient { self.http.put(url).body(body).send().await?; - self.publish_pubky_homeserver(keypair, homeserver).await?; + self.publish_pubky_homeserver(keypair, &homeserver).await?; Ok(()) } @@ -96,10 +101,7 @@ mod tests { let keypair = Keypair::random(); - client - .signup(&keypair, &server.public_key().to_string()) - .await - .unwrap(); + client.signup(&keypair, &server.public_key()).await.unwrap(); let session = client.session(&keypair.public_key()).await.unwrap(); diff --git a/pubky/src/client/pkarr.rs b/pubky/src/native/pkarr.rs similarity index 85% rename from pubky/src/client/pkarr.rs rename to pubky/src/native/pkarr.rs index f22ae43..5cced4b 100644 --- a/pubky/src/client/pkarr.rs +++ b/pubky/src/native/pkarr.rs @@ -1,12 +1,16 @@ -pub use pkarr::{ +use url::Url; + +use pkarr::{ dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, PublicKey, Settings, SignedPacket, + Keypair, PublicKey, SignedPacket, }; use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -use super::{Error, PubkyClient, Result, Url}; +use crate::{ + error::{Error, Result}, + PubkyClient, +}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -38,6 +42,7 @@ impl PubkyClient { /// Resolve a service's public_key and clearnet url from a Pubky domain pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + let original_target = target; // TODO: cache the result of this function? let mut target = target.to_string(); @@ -48,7 +53,11 @@ impl PubkyClient { // PublicKey is very good at extracting the Pkarr TLD from a string. while let Ok(public_key) = PublicKey::try_from(target.clone()) { - let response = self.pkarr.resolve(&public_key).await?; + let response = self + .pkarr + .resolve(&public_key) + .await + .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; let done = parse_pubky_svcb( response, @@ -64,7 +73,7 @@ impl PubkyClient { } } - format_url(homeserver_public_key, host) + format_url(original_target, homeserver_public_key, host) } } @@ -79,6 +88,15 @@ mod tests { }; use pubky_homeserver::Homeserver; + #[tokio::test] + async fn resolve_endpoint() { + let target = "oc9tdmh8c4pmy3tk946oqqfkhic18xdiytadspiy55qhbnja5w9o"; + + let client = PubkyClient::new(); + + client.resolve_endpoint(target).await.unwrap(); + } + #[tokio::test] async fn resolve_homeserver() { let testnet = Testnet::new(3); diff --git a/pubky/src/client/public.rs b/pubky/src/native/public.rs similarity index 92% rename from pubky/src/client/public.rs rename to pubky/src/native/public.rs index 2c1b39f..ce2613e 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/native/public.rs @@ -2,7 +2,7 @@ use bytes::Bytes; use pkarr::PublicKey; -use super::{PubkyClient, Result}; +use crate::{error::Result, PubkyClient}; impl PubkyClient { pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { @@ -67,10 +67,7 @@ mod tests { let keypair = Keypair::random(); - client - .signup(&keypair, &server.public_key().to_string()) - .await - .unwrap(); + client.signup(&keypair, &server.public_key()).await.unwrap(); let response = client .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index e0d87cf..a5d35fc 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -87,6 +87,7 @@ pub fn parse_pubky_svcb( } pub fn format_url( + original_target: &str, homeserver_public_key: Option, host: String, ) -> Result<(PublicKey, Url)> { @@ -100,5 +101,5 @@ pub fn format_url( return Ok((homeserver, Url::parse(&url)?)); } - Err(Error::Generic("Could not resolve endpoint".to_string())) + Err(Error::ResolveEndpoint(original_target.into())) } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index a8d438f..554a4e8 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -4,11 +4,7 @@ pub mod auth; pub mod keys; pub mod pkarr; -#[wasm_bindgen] -pub struct PubkyClient { - pub(crate) http: reqwest::Client, - pub(crate) pkarr: pkarr::PkarrRelayClient, -} +use crate::PubkyClient; #[wasm_bindgen] impl PubkyClient { @@ -16,7 +12,7 @@ impl PubkyClient { pub fn new() -> Self { Self { http: reqwest::Client::new(), - pkarr: pkarr::PkarrRelayClient::default(), + // pkarr: pkarr::PkarrRelayClient::default(), } } } diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index 08b802d..6c9d4ca 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -1,10 +1,14 @@ +use pubky_common::auth::AuthnSignature; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::RequestMode; use pkarr::PkarrRelayClient; -use super::{keys::Keypair, PubkyClient}; +use super::{ + keys::{Keypair, PublicKey}, + PubkyClient, +}; #[wasm_bindgen] impl PubkyClient { @@ -13,16 +17,21 @@ impl PubkyClient { /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" #[wasm_bindgen] - pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result { - let (audience, mut url) = self.resolve_endpoint(homeserver)?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<(), JsValue> { + let keypair = keypair.as_inner(); + let homeserver = homeserver.as_inner().to_string(); + + let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; url.set_path(&format!("/{}", keypair.public_key())); - self.http - .put(&url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())?; + let body = AuthnSignature::generate(keypair, &audience) + .as_bytes() + .to_owned(); - self.publish_pubky_homeserver(keypair, homeserver).await; + self.http.put(url).body(body).send().await?; + + self.publish_pubky_homeserver(keypair, &homeserver).await?; Ok(()) } diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 65beae8..f54f7cb 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -1,5 +1,7 @@ use wasm_bindgen::prelude::*; +use crate::Error; + #[wasm_bindgen] pub struct Keypair(pkarr::Keypair); @@ -48,6 +50,17 @@ impl PublicKey { pub fn to_string(&self) -> String { self.0.to_string() } + + #[wasm_bindgen] + pub fn try_from(value: JsValue) -> Result { + let string = value.as_string().ok_or(Error::Generic( + "Couldn't create a PublicKey from this type of value".to_string(), + ))?; + + Ok(PublicKey( + pkarr::PublicKey::try_from(string).map_err(|e| Error::Pkarr(e))?, + )) + } } impl PublicKey { diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index b000528..4af3e7d 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -1,14 +1,24 @@ +use reqwest::StatusCode; +use url::Url; use wasm_bindgen::prelude::*; pub use pkarr::{ dns::{rdata::SVCB, Packet}, - PkarrRelayClient, PublicKey, SignedPacket, + Keypair, PublicKey, SignedPacket, }; -use crate::error::Result; +use crate::error::{Error, Result}; use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; +use crate::PubkyClient; -use super::{keys::Keypair, PubkyClient}; +const TEST_RELAY: &str = "http://localhost:15411/pkarr"; + +#[macro_export] +macro_rules! log { + ($($arg:expr),*) => { + web_sys::console::debug_1(&format!($($arg),*).into()); + }; +} impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -17,11 +27,91 @@ impl PubkyClient { keypair: &Keypair, host: &str, ) -> Result<()> { - let existing = self.pkarr.resolve(&keypair.public_key().as_inner()).await?; + // let existing = self.pkarr.resolve(&keypair.public_key()).await?; + let existing = self.pkarr_resolve(&keypair.public_key()).await?; - let signed_packet = prepare_packet_for_signup(keypair.as_inner(), host, existing)?; + let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; - self.pkarr.publish(&signed_packet).await?; + // self.pkarr.publish(&signed_packet).await?; + self.pkarr_publish(&signed_packet).await?; + + Ok(()) + } + + /// Resolve the homeserver for a pubky. + pub(crate) async fn resolve_pubky_homeserver( + &self, + pubky: &PublicKey, + ) -> Result<(PublicKey, Url)> { + let target = format!("_pubky.{}", pubky); + + self.resolve_endpoint(&target) + .await + .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) + } + + /// Resolve a service's public_key and clearnet url from a Pubky domain + pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + let original_target = target; + // TODO: cache the result of this function? + + let mut target = target.to_string(); + let mut homeserver_public_key = None; + let mut host = target.clone(); + + let mut step = 0; + + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + let response = self + .pkarr_resolve(&public_key) + .await + .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; + + let done = parse_pubky_svcb( + response, + &public_key, + &mut target, + &mut homeserver_public_key, + &mut host, + &mut step, + ); + + if done { + break; + } + } + + format_url(original_target, homeserver_public_key, host) + } + + //TODO: Allow multiple relays in parallel + //TODO: migrate to pkarr::PkarrRelayClient + async fn pkarr_resolve(&self, public_key: &PublicKey) -> Result> { + let res = self + .http + .get(format!("{TEST_RELAY}/{}", public_key)) + .send() + .await?; + + if res.status() == StatusCode::NOT_FOUND { + return Ok(None); + }; + + // TODO: guard against too large responses. + let bytes = res.bytes().await?; + + let existing = SignedPacket::from_relay_payload(public_key, &bytes)?; + + Ok(Some(existing)) + } + + async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + self.http + .put(format!("{TEST_RELAY}/{}", signed_packet.public_key())) + .body(signed_packet.to_relay_payload()) + .send() + .await?; Ok(()) } From 3cfd876808cca2319523bf02a8ecb320144e7981 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 18:45:24 +0300 Subject: [PATCH 019/125] refactor(pubky): remove unused flume --- pubky-homeserver/src/server.rs | 1 - pubky/Cargo.toml | 6 +----- pubky/src/error.rs | 4 ---- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index f943203..12d497c 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -3,7 +3,6 @@ use std::{ }; use anyhow::{Error, Result}; -use lru::LruCache; use pubky_common::auth::AuthnVerifier; use tokio::{net::TcpListener, signal, sync::Mutex, task::JoinSet}; use tracing::{debug, info, warn}; diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 34e1d5c..6f89a23 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -18,12 +18,11 @@ reqwest = { version = "0.12.5", features = ["cookies"] } bytes = "1.6.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } +flume = "0.11.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pkarr = { version="2.1.0", features = ["async"] } -flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } - [target.'cfg(target_arch = "wasm32")'.dependencies] pkarr = { version = "2.1.0", default-features = false } @@ -48,9 +47,6 @@ tokio = "1.37.0" wasm-bindgen-test = "0.3.42" [features] -async = ["flume/async"] - -default = ["async"] [package.metadata.docs.rs] all-features = true diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 18be027..e885f32 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -25,10 +25,6 @@ pub enum Error { #[error(transparent)] Url(#[from] url::ParseError), - #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Flume(#[from] flume::RecvError), - #[error(transparent)] Reqwest(#[from] reqwest::Error), From e0b58451b54873c2b6681ecc014674f809bb0339 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 19:40:02 +0300 Subject: [PATCH 020/125] refactor(pubky): move pkarr logic to shared --- pubky/src/lib.rs | 2 +- pubky/src/native/auth.rs | 2 +- pubky/src/native/pkarr.rs | 75 ++-------------- pubky/src/shared/pkarr.rs | 177 ++++++++++++++++++++++---------------- pubky/src/wasm/pkarr.rs | 79 ++--------------- 5 files changed, 116 insertions(+), 219 deletions(-) diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 1c35ff8..48bcb81 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -20,5 +20,5 @@ pub use error::Error; pub struct PubkyClient { http: reqwest::Client, #[cfg(not(target_arch = "wasm32"))] - pkarr: PkarrClientAsync, + pub(crate) pkarr: PkarrClientAsync, } diff --git a/pubky/src/native/auth.rs b/pubky/src/native/auth.rs index 76ef81e..23622ad 100644 --- a/pubky/src/native/auth.rs +++ b/pubky/src/native/auth.rs @@ -33,7 +33,7 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [ureq::Error] if + /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if /// the response has any other `>=400` status code. pub async fn session(&self, pubky: &PublicKey) -> Result { let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; diff --git a/pubky/src/native/pkarr.rs b/pubky/src/native/pkarr.rs index 5cced4b..3f11711 100644 --- a/pubky/src/native/pkarr.rs +++ b/pubky/src/native/pkarr.rs @@ -5,75 +5,21 @@ use pkarr::{ Keypair, PublicKey, SignedPacket, }; -use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; - use crate::{ error::{Error, Result}, PubkyClient, }; impl PubkyClient { - /// Publish the SVCB record for `_pubky.`. - pub(crate) async fn publish_pubky_homeserver( + pub(crate) async fn pkarr_resolve( &self, - keypair: &Keypair, - host: &str, - ) -> Result<()> { - let existing = self.pkarr.resolve(&keypair.public_key()).await?; - - let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; - - self.pkarr.publish(&signed_packet).await?; - - Ok(()) + public_key: &PublicKey, + ) -> Result> { + Ok(self.pkarr.resolve(public_key).await?) } - /// Resolve the homeserver for a pubky. - pub(crate) async fn resolve_pubky_homeserver( - &self, - pubky: &PublicKey, - ) -> Result<(PublicKey, Url)> { - let target = format!("_pubky.{}", pubky); - - self.resolve_endpoint(&target) - .await - .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) - } - - /// Resolve a service's public_key and clearnet url from a Pubky domain - pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { - let original_target = target; - // TODO: cache the result of this function? - - let mut target = target.to_string(); - let mut homeserver_public_key = None; - let mut host = target.clone(); - - let mut step = 0; - - // PublicKey is very good at extracting the Pkarr TLD from a string. - while let Ok(public_key) = PublicKey::try_from(target.clone()) { - let response = self - .pkarr - .resolve(&public_key) - .await - .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; - - let done = parse_pubky_svcb( - response, - &public_key, - &mut target, - &mut homeserver_public_key, - &mut host, - &mut step, - ); - - if done { - break; - } - } - - format_url(original_target, homeserver_public_key, host) + pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + Ok(self.pkarr.publish(signed_packet).await?) } } @@ -88,15 +34,6 @@ mod tests { }; use pubky_homeserver::Homeserver; - #[tokio::test] - async fn resolve_endpoint() { - let target = "oc9tdmh8c4pmy3tk946oqqfkhic18xdiytadspiy55qhbnja5w9o"; - - let client = PubkyClient::new(); - - client.resolve_endpoint(target).await.unwrap(); - } - #[tokio::test] async fn resolve_homeserver() { let testnet = Testnet::new(3); diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index a5d35fc..d06b25c 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -5,101 +5,130 @@ use pkarr::{ Keypair, PublicKey, SignedPacket, }; -use crate::error::{Error, Result}; +use crate::{ + error::{Error, Result}, + PubkyClient, +}; const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; -pub fn prepare_packet_for_signup( - keypair: &Keypair, - host: &str, - existing: Option, -) -> Result { - let mut packet = Packet::new_reply(0); +impl PubkyClient { + /// Publish the SVCB record for `_pubky.`. + pub(crate) async fn publish_pubky_homeserver( + &self, + keypair: &Keypair, + host: &str, + ) -> Result<()> { + let existing = self.pkarr_resolve(&keypair.public_key()).await?; - if let Some(existing) = existing { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) + let mut packet = Packet::new_reply(0); + + if let Some(existing) = existing { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } } } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr_publish(&signed_packet).await?; + + Ok(()) } - let svcb = SVCB::new(0, host.try_into()?); + /// Resolve the homeserver for a pubky. + pub(crate) async fn resolve_pubky_homeserver( + &self, + pubky: &PublicKey, + ) -> Result<(PublicKey, Url)> { + let target = format!("_pubky.{}", pubky); - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); + self.resolve_endpoint(&target) + .await + .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) + } - Ok(SignedPacket::from_packet(keypair, &packet)?) -} + /// Resolve a service's public_key and clearnet url from a Pubky domain + pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + let original_target = target; + // TODO: cache the result of this function? -pub fn parse_pubky_svcb( - signed_packet: Option, - public_key: &PublicKey, - target: &mut String, - homeserver_public_key: &mut Option, - host: &mut String, - step: &mut u8, -) -> bool { - *step += 1; + let mut target = target.to_string(); + let mut homeserver_public_key = None; + let mut host = target.clone(); - let mut prior = None; + let mut step = 0; - if let Some(signed_packet) = signed_packet { - for answer in signed_packet.resource_records(target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + step += 1; + + let response = self + .pkarr_resolve(&public_key) + .await + .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; + + let mut prior = None; + + if let Some(signed_packet) = response { + for answer in signed_packet.resource_records(&target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } } - // TODO return random if priority is the same - } else { - prior = Some(svcb) + } + + if let Some(svcb) = prior { + homeserver_public_key = Some(public_key.clone()); + target = svcb.target.to_string(); + + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + host = format!("{target}:{port}"); + } else { + host.clone_from(&target); + }; + + if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION { + continue; + }; } } } - if let Some(svcb) = prior { - *homeserver_public_key = Some(public_key.clone()); - *target = svcb.target.to_string(); - - if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { - if port.len() < 2 { - // TODO: debug! Error encoding port! - } - let port = u16::from_be_bytes([port[0], port[1]]); - - *host = format!("{target}:{port}"); + if let Some(homeserver) = homeserver_public_key { + let url = if host.starts_with("localhost") { + format!("http://{host}") } else { - host.clone_from(target); + format!("https://{host}") }; - return *step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION; + return Ok((homeserver, Url::parse(&url)?)); } + + Err(Error::ResolveEndpoint(original_target.into())) } - - true -} - -pub fn format_url( - original_target: &str, - homeserver_public_key: Option, - host: String, -) -> Result<(PublicKey, Url)> { - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; - - return Ok((homeserver, Url::parse(&url)?)); - } - - Err(Error::ResolveEndpoint(original_target.into())) } diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 4af3e7d..91b85c2 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -8,86 +8,17 @@ pub use pkarr::{ }; use crate::error::{Error, Result}; -use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; use crate::PubkyClient; const TEST_RELAY: &str = "http://localhost:15411/pkarr"; -#[macro_export] -macro_rules! log { - ($($arg:expr),*) => { - web_sys::console::debug_1(&format!($($arg),*).into()); - }; -} - impl PubkyClient { - /// Publish the SVCB record for `_pubky.`. - pub(crate) async fn publish_pubky_homeserver( - &self, - keypair: &Keypair, - host: &str, - ) -> Result<()> { - // let existing = self.pkarr.resolve(&keypair.public_key()).await?; - let existing = self.pkarr_resolve(&keypair.public_key()).await?; - - let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; - - // self.pkarr.publish(&signed_packet).await?; - self.pkarr_publish(&signed_packet).await?; - - Ok(()) - } - - /// Resolve the homeserver for a pubky. - pub(crate) async fn resolve_pubky_homeserver( - &self, - pubky: &PublicKey, - ) -> Result<(PublicKey, Url)> { - let target = format!("_pubky.{}", pubky); - - self.resolve_endpoint(&target) - .await - .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) - } - - /// Resolve a service's public_key and clearnet url from a Pubky domain - pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { - let original_target = target; - // TODO: cache the result of this function? - - let mut target = target.to_string(); - let mut homeserver_public_key = None; - let mut host = target.clone(); - - let mut step = 0; - - // PublicKey is very good at extracting the Pkarr TLD from a string. - while let Ok(public_key) = PublicKey::try_from(target.clone()) { - let response = self - .pkarr_resolve(&public_key) - .await - .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; - - let done = parse_pubky_svcb( - response, - &public_key, - &mut target, - &mut homeserver_public_key, - &mut host, - &mut step, - ); - - if done { - break; - } - } - - format_url(original_target, homeserver_public_key, host) - } - //TODO: Allow multiple relays in parallel //TODO: migrate to pkarr::PkarrRelayClient - async fn pkarr_resolve(&self, public_key: &PublicKey) -> Result> { + pub(crate) async fn pkarr_resolve( + &self, + public_key: &PublicKey, + ) -> Result> { let res = self .http .get(format!("{TEST_RELAY}/{}", public_key)) @@ -106,7 +37,7 @@ impl PubkyClient { Ok(Some(existing)) } - async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { self.http .put(format!("{TEST_RELAY}/{}", signed_packet.public_key())) .body(signed_packet.to_relay_payload()) From 3cc81a5d0e387510c8cacb8513cd479b3ebf867d Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 20:10:03 +0300 Subject: [PATCH 021/125] test(pubky): add headless testing instead of examples --- Cargo.lock | 42 ------ examples/web/auth/.gitignore | 5 - examples/web/auth/index.html | 38 ----- examples/web/auth/package.json | 23 --- examples/web/auth/package.json0 | 30 ---- .../auth/src/components/RecoveryFileUpload.js | 78 ---------- examples/web/auth/src/components/layout.jsx | 21 --- examples/web/auth/src/index.jsx | 21 --- examples/web/auth/src/pages/Home.jsx | 43 ------ examples/web/auth/src/store.js | 90 ------------ examples/web/auth/src/style.css | 137 ------------------ examples/web/auth/vite.config.js | 12 -- examples/web/no-bundler/.gitignore | 3 - examples/web/no-bundler/README.md | 3 - examples/web/no-bundler/index.html | 46 ------ examples/web/no-bundler/package.json | 18 --- pubky/Cargo.toml | 3 - pubky/pkg/README.md | 46 ++++++ pubky/pkg/package.json | 10 +- pubky/pkg/test/auth.js | 26 +--- pubky/pkg/test/keys.js | 2 +- pubky/src/wasm/auth.rs | 36 +++-- 22 files changed, 85 insertions(+), 648 deletions(-) delete mode 100644 examples/web/auth/.gitignore delete mode 100644 examples/web/auth/index.html delete mode 100644 examples/web/auth/package.json delete mode 100644 examples/web/auth/package.json0 delete mode 100644 examples/web/auth/src/components/RecoveryFileUpload.js delete mode 100644 examples/web/auth/src/components/layout.jsx delete mode 100644 examples/web/auth/src/index.jsx delete mode 100644 examples/web/auth/src/pages/Home.jsx delete mode 100644 examples/web/auth/src/store.js delete mode 100644 examples/web/auth/src/style.css delete mode 100644 examples/web/auth/vite.config.js delete mode 100644 examples/web/no-bundler/.gitignore delete mode 100644 examples/web/no-bundler/README.md delete mode 100644 examples/web/no-bundler/index.html delete mode 100644 examples/web/no-bundler/package.json diff --git a/Cargo.lock b/Cargo.lock index 8d6ec73..ed8e5db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,16 +372,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1583,7 +1573,6 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", "web-sys", ] @@ -1890,12 +1879,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -2625,31 +2608,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "wasm-bindgen-test" -version = "0.3.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "scoped-tls", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "web-sys" version = "0.3.69" diff --git a/examples/web/auth/.gitignore b/examples/web/auth/.gitignore deleted file mode 100644 index fb59162..0000000 --- a/examples/web/auth/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.netlify -.parcel* -dist -node_modules -package-lock.json diff --git a/examples/web/auth/index.html b/examples/web/auth/index.html deleted file mode 100644 index ef33b63..0000000 --- a/examples/web/auth/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - Pubky demo - - - - - - - - - - - - - - - - - - - - - - - -
- - - diff --git a/examples/web/auth/package.json b/examples/web/auth/package.json deleted file mode 100644 index 61e1abc..0000000 --- a/examples/web/auth/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "private": "true", - "main": "src/index.js", - "type": "module", - "scripts": { - "build": "vite build", - "start": "vite serve", - "preview": "vite preview", - "preinstall": "cargo run --bin bundle_pubky_npm" - }, - "dependencies": { - "@synonymdev/pubky": "file:../../../pubky/pkg", - - "@solidjs/router": "^0.10.9", - "solid-js": "^1.7.0", - "vite-plugin-solid": "^2.10.2" - }, - "devDependencies": { - "typescript": "^4.9.5", - "vite": "^4.1.4", - "vite-plugin-html": "^3.2.0" - } -} diff --git a/examples/web/auth/package.json0 b/examples/web/auth/package.json0 deleted file mode 100644 index 9bd267d..0000000 --- a/examples/web/auth/package.json0 +++ /dev/null @@ -1,30 +0,0 @@ -{ - "private": "true", - "type": "module", - "source": "src/index.html", - "browserslist": "> 0.5%, last 2 versions, not dead", - "scripts": { - "start": "parcel -p 7251", - "serve": "npm run start", - "build": "parcel build", - "lint": "standard --fix" - }, - "standard": { - "ignore": [ - "src/pages", - "src/components", - "dist" - ] - }, - "dependencies": { - "@solidjs/router": "^0.10.9", - "parcel": "^2.0.1", - "@synonymdev/pubky": "file:../../../pubky/pkg", - "solid-js": "^1.7.0" - }, - "devDependencies": { - "@babel/core": "^7.24.9", - "babel-preset-solid": "^1.2.5", - "solid-refresh": "^0.2.2" - } -} diff --git a/examples/web/auth/src/components/RecoveryFileUpload.js b/examples/web/auth/src/components/RecoveryFileUpload.js deleted file mode 100644 index 06b3ad0..0000000 --- a/examples/web/auth/src/components/RecoveryFileUpload.js +++ /dev/null @@ -1,78 +0,0 @@ -import { useNavigate } from '@solidjs/router' -import { createSignal } from 'solid-js' -import { crypto } from '@pubky/common' - -import { decryptRecoveryFile } from '../sdk/recovery.js' -import store from '../store.js' - -/** - * @param {"login" | "signup"} type - */ -const RecoveryFileUpload = ({ type }) => { - const [valid, setValid] = createSignal(false) - - const navigate = useNavigate() - - const onSubmit = async (e) => { - e.preventDefault() - - const form = e.target - - const file = form.file.files[0] - const passphrase = form['import-passphrase'].value - - const reader = new FileReader() - - reader.onload = async function(event) { - const recoveryFile = event.target.result - - const seedResult = await decryptRecoveryFile(recoveryFile, passphrase) - if (seedResult.isErr()) return alert(seedResult.error.message) - - const action = type === 'signup' - ? store.pubkyClient.signup.bind(store.pubkyClient) - : store.pubkyClient.login.bind(store.pubkyClient); - - const result = await action(seedResult.value) - crypto.zeroize(seedResult.value) - - if (result.isErr()) return alert(result.error.message) - - store.setCurrentUser({ id: result.value }) - - navigate("/", { replace: true }) - } - - // Read the file as text - reader.readAsText(file) - } - - const onUpdate = (e) => { - const form = e.target.parentElement.parentElement - - const file = form.file.files[0] - const passphrase = form['import-passphrase'].value - - if (passphrase.length > 0 && file) { - setValid(true) - } else { - setValid(false) - } - } - - return ( -
- - - -
- ) -} - -export default RecoveryFileUpload diff --git a/examples/web/auth/src/components/layout.jsx b/examples/web/auth/src/components/layout.jsx deleted file mode 100644 index 1d0c03b..0000000 --- a/examples/web/auth/src/components/layout.jsx +++ /dev/null @@ -1,21 +0,0 @@ -const Layout = ({ children }) => { - return ( - <> -
-
-

Pubky

-
-
- -
- {children} -
- -
-

This is a proof of concept for demonstration purposes only.

-
- - ) -} - -export default Layout diff --git a/examples/web/auth/src/index.jsx b/examples/web/auth/src/index.jsx deleted file mode 100644 index 1c78d71..0000000 --- a/examples/web/auth/src/index.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import { render } from 'solid-js/web' -import { Router, Route } from '@solidjs/router' - -import { PubkyClient } from "@synonymdev/pubky"; - -let client = new PubkyClient() -console.log(client); - -import Home from './pages/Home.jsx' -// import Login from './pages/Login.js' -// import Signup from './pages/Signup.js' - -render(() => ( - - - -), document.getElementById('app')) - -// -// -// diff --git a/examples/web/auth/src/pages/Home.jsx b/examples/web/auth/src/pages/Home.jsx deleted file mode 100644 index 307d1a1..0000000 --- a/examples/web/auth/src/pages/Home.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useNavigate } from '@solidjs/router' - -import store from '../store.js' -import Layout from '../components/Layout' - -const Home = () => { - const navigate = useNavigate() - - let currentUser = store.getCurrentUser() - - if (!currentUser) { - navigate('/login', { replace: true }) - } - - const logout = async () => { - // await store.pubkyClient.ready() - // - // const logoutResult = await store.pubkyClient.logout(currentUser.id) - // if (logoutResult.isErr()) { - // alert(logoutResult.error.message) - // return - // } - // - // store.removeCurrentUser() - // - // if (window.location.pathname === '/home') { - // navigate('/', { replace: true }) - // } else { - // navigate('/home', { replace: true }) - // } - } - - return ( - - Home.. -

Welcome {store.getCurrentUser()?.id}

-
- -
- ) -} - -export default Home diff --git a/examples/web/auth/src/store.js b/examples/web/auth/src/store.js deleted file mode 100644 index 910d6a5..0000000 --- a/examples/web/auth/src/store.js +++ /dev/null @@ -1,90 +0,0 @@ -import { createMutable } from 'solid-js/store' -// import z32 from 'z32' -// import { Result } from '@pubky/common' -// import { Level } from 'level' -// import Client from '@pubky/client' -// -// import { recoveryFile } from './sdk/recovery.js' -// -// // In real application it should be the server's Pkarr Id -// const DEFAULT_HOME_SERVER = 'http://localhost:7259' -// const DEFAULT_RELAY = 'https://relay.pkarr.org' - -class Store { - constructor() { - // this.db = new Level('app-db', { keyEncoding: 'utf8', valueEncoding: 'json' }) - // this.currentUser = null - // - // this.pubkyClient = new Client(DEFAULT_HOME_SERVER, { relay: DEFAULT_RELAY }) - // - // this.DEFAULT_HOME_SERVER = DEFAULT_HOME_SERVER - } - - // getUsers() { - // try { - // return JSON.parse(global.localStorage.getItem('users')) || [] - // } catch { - // return [] - // } - // } - - getCurrentUser() { - try { - return JSON.parse(global.localStorage.getItem('currentUser')) - } catch { - return null - } - } - - // setCurrentUser(user) { - // const users = this.getUsers() - // - // if (!users?.map(user => user.id).includes(user.id)) { - // global.localStorage.setItem('users', JSON.stringify([ - // ...users, - // user - // ])) - // } - // - // global.localStorage.setItem('currentUser', JSON.stringify(user)) - // } - // - // removeCurrentUser() { - // global.localStorage.removeItem('currentUser') - // } - // - // /** - // * @param {string} name - // * @param {string} passphrase - // * - // * @returns {Promise>} - // */ - // async createAccount(name, passphrase) { - // await this.pubkyClient.ready() - // - // const seed = Client.crypto.generateSeed() - // - // const keypair = Client.crypto.generateKeyPair(seed) - // Client.crypto.zeroize(keypair.secretKey) - // - // const userId = z32.encode(keypair.publicKey) - // - // const recoveryFileAndFilename = await recoveryFile(name, seed, passphrase) - // - // const signedUp = await this.pubkyClient.signup(seed) - // if (signedUp.isErr()) return signedUp - // - // Client.crypto.zeroize(seed) - // - // return Result.Ok({ - // userId, - // ...recoveryFileAndFilename - // }) - // } -} - -export default createMutable(new Store()) diff --git a/examples/web/auth/src/style.css b/examples/web/auth/src/style.css deleted file mode 100644 index 26c3d1d..0000000 --- a/examples/web/auth/src/style.css +++ /dev/null @@ -1,137 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, -body, -#app { - width: 100%; - height: 100%; - min-width: 320px; -} - -body, -#app { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: 600px; - margin: 0 auto; - padding: 2rem 1rem 0; - font-family: 'IBM Plex Sans', Helvetica, sans-serif; -} - -.button { - background: none; - border: none; - cursor: pointer; -} - -.button:disabled { - pointer-events: none; - opacity: 0.4; -} - -main { - height: 100%; - margin: 1rem 0; -} - -footer { - text-align: center; - color: #666; - padding: 1rem 0; - border-top: 1px solid #ddd; - margin-top: 1rem; -} - -footer p, -footer a { - font-size: 0.8rem !important; - padding-bottom: 0.3rem; -} - -.row { - display: flex; - justify-content: space-between; - align-items: baseline; -} - -a { - color: #000; - font-size: 1rem; - font-weight: 700; -} - -h1 { - font-size: 2rem; - font-weight: 700; -} - -.small { - font-size: 0.8rem; -} - -.button.primary { - background: black; - color: white; - - min-width: 90px; - width: 100%; - - padding: 0.4rem 0.8rem; - border: 2px solid #000; -} - -.button.primary:hover { - background: #222; -} - -.divider { - background: #333; - height: 1px; - margin-top: 1rem; - margin-bottom: 1rem; -} - -form { - display: flex; - flex-direction: column; -} - -label { - font-size: 0.9rem; - font-weight: 700; - margin-bottom: 1rem; -} - -form input { - font-size: 1rem; - width: 100%; - height: 2rem; - border: none; - box-shadow: none; - border-radius: 0; - border-bottom: 1px solid #ddd; -} - -form input:focus { - outline: none; - border-bottom: 2px solid #000; -} - -label.checkbox { - display: flex; - font-weight: normal; - line-height: 1rem; -} - -label.checkbox input { - width: 1rem; - margin: 0 0.5rem 0 0; -} -label.checkbox:focus-within { - outline: 1px solid; -} diff --git a/examples/web/auth/vite.config.js b/examples/web/auth/vite.config.js deleted file mode 100644 index 6dba9ca..0000000 --- a/examples/web/auth/vite.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "vite"; -// import wasmPack from "vite-plugin-wasm-pack"; -import solidPlugin from 'vite-plugin-solid'; -// import path from "node:path"; - -export default defineConfig({ - // pass your local crate path to the plugin - plugins: [ - // wasmPack(path.resolve("../../../pubky")), - solidPlugin() - ], -}); diff --git a/examples/web/no-bundler/.gitignore b/examples/web/no-bundler/.gitignore deleted file mode 100644 index 91a3983..0000000 --- a/examples/web/no-bundler/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules -package-lock.json diff --git a/examples/web/no-bundler/README.md b/examples/web/no-bundler/README.md deleted file mode 100644 index ddd6280..0000000 --- a/examples/web/no-bundler/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# No bundler - -An example of using Pubky wasm immidiatly in an `index.html` with no bundlers. diff --git a/examples/web/no-bundler/index.html b/examples/web/no-bundler/index.html deleted file mode 100644 index 0770e09..0000000 --- a/examples/web/no-bundler/index.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - Pubky demo - - - - - - - - - - - - - - - - - - - - - - - -
- - - diff --git a/examples/web/no-bundler/package.json b/examples/web/no-bundler/package.json deleted file mode 100644 index 68d5fbc..0000000 --- a/examples/web/no-bundler/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "scripts": { - "start": "vite serve", - "preinstall": "cargo run --bin bundle_pubky_npm" - }, - "dependencies": { - "@synonymdev/pubky": "file:../../../pubky/pkg", - - "@solidjs/router": "^0.10.9", - "solid-js": "^1.7.0" - }, - "devDependencies": { - "typescript": "^4.9.5", - "vite": "^4.1.4", - "vite-plugin-html": "^3.2.0", - "vite-plugin-solid": "^2.10.2" - } -} diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 6f89a23..09d2a44 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -43,9 +43,6 @@ web-sys = { version = "0.3.69", features = [ pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.42" - [features] [package.metadata.docs.rs] diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 32080e5..d965c2f 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -1,3 +1,49 @@ # Pubky JavaScript implementation of [Pubky](https://github.com/pubky/pubky). + +## Install + +```bash +npm install @synonymdev/pubky +``` + +## Getting started + +```js +import PubkyClient from "@synonymdev/pubky"; + +// Initialize PubkyClient with Pkarr relay(s). +let client = new PubkyClient(); + +// Generate a keypair +let keypair = Keypair.random(); + +// Create a new account +let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); + +await client.signup(keypair, homeserver) +``` + +## Test and Development + +For test and development, you can run a local homeserver in a test network. + +If you don't have Cargo Installed, start by installing it: + +```bash +curl https://sh.rustup.rs -sSf | sh +``` + +Clone the Pubky repository: + +```bash +git clone https://github.com/pubky/pubky +cd pubky/ +``` + +Run the testnet server + +```bash +cargo run --bin pubky_homeserver -- --testnet +``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index b3e43e9..6532b95 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -10,7 +10,8 @@ }, "scripts": { "lint": "standard --fix", - "test": "brittle test/*.js -cov", + "test": "tape test/*.js -cov", + "test-browser": "browserify test/*.js -p esmify | npx tape-run", "preinstall": "cargo run --bin bundle_pubky_npm", "prepublishOnly": "npm run lint && npm run test" }, @@ -33,7 +34,10 @@ "identity" ], "devDependencies": { - "brittle": "^3.6.1", - "standard": "^17.1.0" + "browser-resolve": "^2.0.0", + "esmify": "^2.1.1", + "standard": "^17.1.0", + "tape": "^5.8.1", + "tape-run": "^11.0.0" } } diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 23cfe8f..fd17015 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -1,4 +1,4 @@ -import test from 'brittle' +import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' @@ -7,30 +7,14 @@ test('seed auth', async (t) => { let client = new PubkyClient(); let keypair = Keypair.random(); - let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); + let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); await client.signup(keypair, homeserver); - // const client = new Client( - // homeserver.homeserver.pkarr.serverPkarr.publicKey(), - // { - // relay: homeserver.testnet.relay - // } - // ) - // await client.ready() - // - // const seed = Client.crypto.generateSeed() - // const keypair = Client.crypto.generateKeyPair(seed) - // const expectedUserId = keypair.public_key().to_string() - // - // const userIdResult = await client.signup(seed) - // t.ok(userIdResult.isOk(), userIdResult.error) - // - // const userId = userIdResult.value - // t.is(userId, expectedUserId) - // + t.ok(true); + // const session = await client.session() - // t.ok(session?.users[userId]) + // t.ok(session) // // { // await client.logout(userId) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index d01467a..76e3202 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -1,4 +1,4 @@ -import test from 'brittle' +import test from 'tape' import { Keypair } from '../index.js' diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index 6c9d4ca..f5d60c6 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -1,10 +1,15 @@ -use pubky_common::auth::AuthnSignature; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::RequestMode; +use reqwest::StatusCode; + use pkarr::PkarrRelayClient; +use pubky_common::{auth::AuthnSignature, session::Session}; + +use crate::Error; + use super::{ keys::{Keypair, PublicKey}, PubkyClient, @@ -35,18 +40,29 @@ impl PubkyClient { Ok(()) } -} -#[cfg(test)] -mod tests { - use wasm_bindgen_test::wasm_bindgen_test; + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if + /// the response has any other `>=400` status code. + #[wasm_bindgen] + pub async fn session(&self, pubky: &PublicKey) -> Result { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + url.set_path(&format!("/{}/session", pubky)); - use super::*; + let res = self.http.get(url).send().await?; - #[wasm_bindgen_test] - async fn basic() { - // let client = PubkyClient::new(); + if res.status() == StatusCode::NOT_FOUND { + return Err(Error::NotSignedIn); + } + + if !res.status().is_success() { + res.error_for_status_ref()?; + }; + + let bytes = res.bytes().await?; + + Ok(Session::deserialize(&bytes)?) } } From d050866cceefb3fa3b63c7b6231009695c31804b Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 23:24:32 +0300 Subject: [PATCH 022/125] feat(pubky): auth working everywhere except nodejs --- Cargo.lock | 21 +++++- pubky-homeserver/Cargo.toml | 2 +- pubky-homeserver/src/routes.rs | 6 +- pubky-homeserver/src/routes/auth.rs | 29 ++++++-- pubky/pkg/test/auth.js | 40 ++++++----- pubky/src/error.rs | 6 +- pubky/src/native.rs | 61 ++++++++++++++--- pubky/src/native/pkarr.rs | 92 -------------------------- pubky/src/native/public.rs | 7 +- pubky/src/{native => shared}/auth.rs | 59 ++++++++++------- pubky/src/{shared.rs => shared/mod.rs} | 1 + pubky/src/shared/pkarr.rs | 70 +++++++++++++++++++- pubky/src/wasm.rs | 58 ++++++++++++++-- pubky/src/wasm/auth.rs | 68 ------------------- pubky/src/wasm/keys.rs | 2 +- pubky/src/wasm/pkarr.rs | 11 ++- pubky/src/wasm/session.rs | 6 ++ 17 files changed, 291 insertions(+), 248 deletions(-) delete mode 100644 pubky/src/native/pkarr.rs rename pubky/src/{native => shared}/auth.rs (63%) rename pubky/src/{shared.rs => shared/mod.rs} (51%) delete mode 100644 pubky/src/wasm/auth.rs create mode 100644 pubky/src/wasm/session.rs diff --git a/Cargo.lock b/Cargo.lock index ed8e5db..dbeea1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,7 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http", @@ -205,6 +206,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -348,7 +361,7 @@ version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -884,6 +897,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 698a3e6..eaba493 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.82" -axum = "0.7.5" +axum = { version = "0.7.5", features = ["macros"] } axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] } base32 = "0.5.1" bytes = "1.6.1" diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 3f53d9b..0d3b7e3 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -41,10 +41,6 @@ pub fn create_app(state: AppState) -> Router { base(state.clone()) // TODO: Only enable this for test environments? .nest("/pkarr", pkarr_router(state)) - .layer( - CorsLayer::new() - .allow_methods([Method::GET, Method::PUT, Method::POST, Method::DELETE]) - .allow_origin(cors::Any), - ) + .layer(CorsLayer::very_permissive()) .layer(TraceLayer::new_for_http()) } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index c38aa38..60761fb 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -1,6 +1,7 @@ use axum::{ + debug_handler, extract::{Request, State}, - http::{HeaderMap, StatusCode}, + http::{uri::Scheme, HeaderMap, StatusCode, Uri}, response::IntoResponse, Router, }; @@ -8,7 +9,7 @@ use axum_extra::{headers::UserAgent, TypedHeader}; use bytes::Bytes; use heed::BytesEncode; use postcard::to_allocvec; -use tower_cookies::{Cookie, Cookies}; +use tower_cookies::{cookie::SameSite, Cookie, Cookies}; use pubky_common::{ crypto::{random_bytes, random_hash}, @@ -26,16 +27,26 @@ use crate::{ server::AppState, }; +#[debug_handler] pub async fn signup( State(state): State, TypedHeader(user_agent): TypedHeader, cookies: Cookies, pubky: Pubky, + uri: Uri, body: Bytes, ) -> Result { // TODO: Verify invitation link. // TODO: add errors in case of already axisting user. - signin(State(state), TypedHeader(user_agent), cookies, pubky, body).await + signin( + State(state), + TypedHeader(user_agent), + cookies, + pubky, + uri, + body, + ) + .await } pub async fn session( @@ -57,6 +68,7 @@ pub async fn session( let session = session.to_owned(); rtxn.commit()?; + // TODO: add content-type return Ok(session); }; @@ -95,6 +107,7 @@ pub async fn signin( TypedHeader(user_agent): TypedHeader, cookies: Cookies, pubky: Pubky, + uri: Uri, body: Bytes, ) -> Result { let public_key = pubky.public_key(); @@ -135,7 +148,15 @@ pub async fn signin( sessions.put(&mut wtxn, &session_secret, &session.serialize())?; - cookies.add(Cookie::new(public_key.to_string(), session_secret)); + let mut cookie = Cookie::new(public_key.to_string(), session_secret); + cookie.set_path("/"); + if *uri.scheme().unwrap_or(&Scheme::HTTP) == Scheme::HTTPS { + cookie.set_secure(true); + cookie.set_same_site(SameSite::None); + } + cookie.set_http_only(true); + + cookies.add(cookie); wtxn.commit()?; diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index fd17015..a660d99 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -3,30 +3,28 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' test('seed auth', async (t) => { + const client = new PubkyClient() - let client = new PubkyClient(); + const keypair = Keypair.random() + const publicKey = keypair.public_key() - let keypair = Keypair.random(); + const homeserver = PublicKey.try_from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + await client.signup(keypair, homeserver) - let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); - await client.signup(keypair, homeserver); + const session = await client.session(publicKey) + t.ok(session) - t.ok(true); + { + await client.signout(publicKey) - // const session = await client.session() - // t.ok(session) - // - // { - // await client.logout(userId) - // - // const session = await client.session() - // t.absent(session?.users?.[userId]) - // } - // - // { - // await client.login(seed) - // - // const session = await client.session() - // t.ok(session?.users[userId]) - // } + const session = await client.session(publicKey) + t.notOk(session) + } + + { + await client.signin(keypair) + + const session = await client.session(publicKey) + t.ok(session) + } }) diff --git a/pubky/src/error.rs b/pubky/src/error.rs index e885f32..501168d 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -12,9 +12,6 @@ pub enum Error { #[error("Generic error: {0}")] Generic(String), - #[error("Not signed in")] - NotSignedIn, - // === Transparent === #[error(transparent)] Dns(#[from] SimpleDnsError), @@ -29,7 +26,6 @@ pub enum Error { Reqwest(#[from] reqwest::Error), #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), #[error("Could not resolve endpoint for {0}")] @@ -37,7 +33,7 @@ pub enum Error { } #[cfg(target_arch = "wasm32")] -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use wasm_bindgen::JsValue; #[cfg(target_arch = "wasm32")] impl From for JsValue { diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 90a1930..fa2ae3f 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,22 +1,31 @@ -pub mod auth; -pub mod pkarr; pub mod public; use std::time::Duration; use ::pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PkarrClientAsync, Settings, + PkarrClient, PublicKey, Settings, SignedPacket, }; +use pkarr::Keypair; +use pubky_common::session::Session; +use reqwest::{Method, RequestBuilder}; +use url::Url; -use crate::PubkyClient; +use crate::{error::Result, PubkyClient}; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +impl Default for PubkyClient { + fn default() -> Self { + Self::new() + } +} + impl PubkyClient { pub fn new() -> Self { Self { http: reqwest::Client::builder() + .cookie_store(true) .user_agent(DEFAULT_USER_AGENT) .build() .unwrap(), @@ -25,7 +34,6 @@ impl PubkyClient { } } - #[cfg(not(target_arch = "wasm32"))] pub fn test(testnet: &Testnet) -> Self { Self { http: reqwest::Client::builder() @@ -45,10 +53,45 @@ impl PubkyClient { .as_async(), } } -} -impl Default for PubkyClient { - fn default() -> Self { - Self::new() + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<()> { + self.inner_signup(keypair, homeserver).await + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if + /// the response has any other `>=400` status code. + pub async fn session(&self, pubky: &PublicKey) -> Result> { + self.inner_session(pubky).await + } + + /// Signout from a homeserver. + pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { + self.inner_signout(pubky).await + } + + /// Signin to a homeserver. + pub async fn signin(&self, keypair: &Keypair) -> Result<()> { + self.inner_signin(keypair).await + } + + pub(crate) async fn pkarr_resolve( + &self, + public_key: &PublicKey, + ) -> Result> { + Ok(self.pkarr.resolve(public_key).await?) + } + + pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + Ok(self.pkarr.publish(signed_packet).await?) + } + + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { + self.http.request(method, url) } } diff --git a/pubky/src/native/pkarr.rs b/pubky/src/native/pkarr.rs deleted file mode 100644 index 3f11711..0000000 --- a/pubky/src/native/pkarr.rs +++ /dev/null @@ -1,92 +0,0 @@ -use url::Url; - -use pkarr::{ - dns::{rdata::SVCB, Packet}, - Keypair, PublicKey, SignedPacket, -}; - -use crate::{ - error::{Error, Result}, - PubkyClient, -}; - -impl PubkyClient { - pub(crate) async fn pkarr_resolve( - &self, - public_key: &PublicKey, - ) -> Result> { - Ok(self.pkarr.resolve(public_key).await?) - } - - pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { - Ok(self.pkarr.publish(signed_packet).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use pkarr::{ - dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, Settings, SignedPacket, - }; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn resolve_homeserver() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - // Publish an intermediate controller of the homeserver - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - ..Default::default() - }, - ..Default::default() - }) - .unwrap() - .as_async(); - - let intermediate = Keypair::random(); - - let mut packet = Packet::new_reply(0); - - let server_tld = server.public_key().to_string(); - - let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); - - pkarr_client.publish(&signed_packet).await.unwrap(); - - { - let client = PubkyClient::test(&testnet); - - let pubky = Keypair::random(); - - client - .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())) - .await - .unwrap(); - - let (public_key, url) = client - .resolve_pubky_homeserver(&pubky.public_key()) - .await - .unwrap(); - - assert_eq!(public_key, server.public_key()); - assert_eq!(url.host_str(), Some("localhost")); - assert_eq!(url.port(), Some(server.port())); - } - } -} diff --git a/pubky/src/native/public.rs b/pubky/src/native/public.rs index ce2613e..9b2a271 100644 --- a/pubky/src/native/public.rs +++ b/pubky/src/native/public.rs @@ -50,12 +50,10 @@ fn normalize_path(path: &str) -> String { #[cfg(test)] mod tests { - use std::ops::Deref; use crate::*; use pkarr::{mainline::Testnet, Keypair}; - use pubky_common::session::Session; use pubky_homeserver::Homeserver; #[tokio::test] @@ -69,9 +67,10 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); - let response = client + client .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await; + .await + .unwrap(); let response = client .get(&keypair.public_key(), "/pub/foo.txt") diff --git a/pubky/src/native/auth.rs b/pubky/src/shared/auth.rs similarity index 63% rename from pubky/src/native/auth.rs rename to pubky/src/shared/auth.rs index 23622ad..e78f84a 100644 --- a/pubky/src/native/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,4 +1,4 @@ -use reqwest::StatusCode; +use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; @@ -13,7 +13,11 @@ impl PubkyClient { /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" - pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<()> { + pub(crate) async fn inner_signup( + &self, + keypair: &Keypair, + homeserver: &PublicKey, + ) -> Result<()> { let homeserver = homeserver.to_string(); let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; @@ -24,7 +28,7 @@ impl PubkyClient { .as_bytes() .to_owned(); - self.http.put(url).body(body).send().await?; + self.request(Method::PUT, url).body(body).send().await?; self.publish_pubky_homeserver(keypair, &homeserver).await?; @@ -33,17 +37,17 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. - pub async fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; + /// Returns None if not signed in, or [reqwest::Error] + /// if the response has any other `>=404` status code. + pub(crate) async fn inner_session(&self, pubky: &PublicKey) -> Result> { + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - let res = self.http.get(url).send().await?; + let res = self.request(Method::GET, url).send().await?; if res.status() == StatusCode::NOT_FOUND { - return Err(Error::NotSignedIn); + return Ok(None); } if !res.status().is_success() { @@ -52,22 +56,22 @@ impl PubkyClient { let bytes = res.bytes().await?; - Ok(Session::deserialize(&bytes)?) + Ok(Some(Session::deserialize(&bytes)?)) } /// Signout from a homeserver. - pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; + pub async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - self.http.delete(url).send().await?; + self.request(Method::DELETE, url).send().await?; Ok(()) } /// Signin to a homeserver. - pub async fn signin(&self, keypair: &Keypair) -> Result<()> { + pub async fn inner_signin(&self, keypair: &Keypair) -> Result<()> { let pubky = keypair.public_key(); let (audience, mut url) = self.resolve_pubky_homeserver(&pubky).await?; @@ -78,7 +82,7 @@ impl PubkyClient { .as_bytes() .to_owned(); - self.http.post(url).body(body).send().await?; + self.request(Method::POST, url).body(body).send().await?; Ok(()) } @@ -86,11 +90,15 @@ impl PubkyClient { #[cfg(test)] mod tests { + + use std::time::Duration; + use crate::*; use pkarr::{mainline::Testnet, Keypair}; use pubky_common::session::Session; use pubky_homeserver::Homeserver; + use tokio::time::sleep; #[tokio::test] async fn basic_authn() { @@ -103,27 +111,30 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); - let session = client.session(&keypair.public_key()).await.unwrap(); + let session = client + .session(&keypair.public_key()) + .await + .unwrap() + .unwrap(); assert_eq!(session, Session { ..session.clone() }); client.signout(&keypair.public_key()).await.unwrap(); { - let session = client.session(&keypair.public_key()).await; + let session = client.session(&keypair.public_key()).await.unwrap(); - assert!(session.is_err()); - - match session { - Err(Error::NotSignedIn) => {} - _ => panic!("expected NotSignedInt error"), - } + assert!(session.is_none()); } client.signin(&keypair).await.unwrap(); { - let session = client.session(&keypair.public_key()).await.unwrap(); + let session = client + .session(&keypair.public_key()) + .await + .unwrap() + .unwrap(); assert_eq!(session, Session { ..session.clone() }); } diff --git a/pubky/src/shared.rs b/pubky/src/shared/mod.rs similarity index 51% rename from pubky/src/shared.rs rename to pubky/src/shared/mod.rs index ad39b70..c61bbfe 100644 --- a/pubky/src/shared.rs +++ b/pubky/src/shared/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod pkarr; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index d06b25c..879e901 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -77,7 +77,7 @@ impl PubkyClient { let response = self .pkarr_resolve(&public_key) .await - .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; + .map_err(|_| Error::ResolveEndpoint(original_target.into()))?; let mut prior = None; @@ -132,3 +132,71 @@ impl PubkyClient { Err(Error::ResolveEndpoint(original_target.into())) } } + +#[cfg(test)] +mod tests { + use super::*; + + use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, Settings, SignedPacket, + }; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn resolve_homeserver() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + // Publish an intermediate controller of the homeserver + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let intermediate = Keypair::random(); + + let mut packet = Packet::new_reply(0); + + let server_tld = server.public_key().to_string(); + + let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + { + let client = PubkyClient::test(&testnet); + + let pubky = Keypair::random(); + + client + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())) + .await + .unwrap(); + + let (public_key, url) = client + .resolve_pubky_homeserver(&pubky.public_key()) + .await + .unwrap(); + + assert_eq!(public_key, server.public_key()); + assert_eq!(url.host_str(), Some("localhost")); + assert_eq!(url.port(), Some(server.port())); + } + } +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 554a4e8..01fcf73 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,18 +1,66 @@ use wasm_bindgen::prelude::*; -pub mod auth; -pub mod keys; -pub mod pkarr; +use reqwest::{Method, RequestBuilder}; +use url::Url; use crate::PubkyClient; +mod keys; +mod pkarr; +mod session; + +use keys::{Keypair, PublicKey}; +use session::Session; + #[wasm_bindgen] impl PubkyClient { #[wasm_bindgen(constructor)] pub fn new() -> Self { Self { - http: reqwest::Client::new(), - // pkarr: pkarr::PkarrRelayClient::default(), + http: reqwest::Client::builder().build().unwrap(), } } + + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + #[wasm_bindgen] + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<(), JsValue> { + self.inner_signup(keypair.as_inner(), homeserver.as_inner()) + .await + .map_err(|e| e.into()) + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if + /// the response has any other `>=400` status code. + #[wasm_bindgen] + pub async fn session(&self, pubky: &PublicKey) -> Result, JsValue> { + self.inner_session(pubky.as_inner()) + .await + .map(|s| s.map(|s| Session(s).into())) + .map_err(|e| e.into()) + } + + /// Signout from a homeserver. + #[wasm_bindgen] + pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> { + self.inner_signout(pubky.as_inner()) + .await + .map_err(|e| e.into()) + } + + /// Signin to a homeserver. + #[wasm_bindgen] + pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> { + self.inner_signin(keypair.as_inner()) + .await + .map_err(|e| e.into()) + } + + pub(crate) fn request(&self, method: Method, url: Url) -> reqwest::RequestBuilder { + self.http.request(method, url).fetch_credentials_include() + } } diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs deleted file mode 100644 index f5d60c6..0000000 --- a/pubky/src/wasm/auth.rs +++ /dev/null @@ -1,68 +0,0 @@ -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use web_sys::RequestMode; - -use reqwest::StatusCode; - -use pkarr::PkarrRelayClient; - -use pubky_common::{auth::AuthnSignature, session::Session}; - -use crate::Error; - -use super::{ - keys::{Keypair, PublicKey}, - PubkyClient, -}; - -#[wasm_bindgen] -impl PubkyClient { - /// Signup to a homeserver and update Pkarr accordingly. - /// - /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key - /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" - #[wasm_bindgen] - pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<(), JsValue> { - let keypair = keypair.as_inner(); - let homeserver = homeserver.as_inner().to_string(); - - let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; - - url.set_path(&format!("/{}", keypair.public_key())); - - let body = AuthnSignature::generate(keypair, &audience) - .as_bytes() - .to_owned(); - - self.http.put(url).body(body).send().await?; - - self.publish_pubky_homeserver(keypair, &homeserver).await?; - - Ok(()) - } - - /// Check the current sesison for a given Pubky in its homeserver. - /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. - #[wasm_bindgen] - pub async fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{}/session", pubky)); - - let res = self.http.get(url).send().await?; - - if res.status() == StatusCode::NOT_FOUND { - return Err(Error::NotSignedIn); - } - - if !res.status().is_success() { - res.error_for_status_ref()?; - }; - - let bytes = res.bytes().await?; - - Ok(Session::deserialize(&bytes)?) - } -} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index f54f7cb..cbd6ec7 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -9,7 +9,7 @@ pub struct Keypair(pkarr::Keypair); impl Keypair { #[wasm_bindgen] /// Generate a random [Keypair] - pub fn random(secret_key: js_sys::Uint8Array) -> Self { + pub fn random() -> Self { Self(pkarr::Keypair::random()) } diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 91b85c2..8b43299 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -1,17 +1,14 @@ use reqwest::StatusCode; -use url::Url; -use wasm_bindgen::prelude::*; -pub use pkarr::{ - dns::{rdata::SVCB, Packet}, - Keypair, PublicKey, SignedPacket, -}; +pub use pkarr::{PublicKey, SignedPacket}; -use crate::error::{Error, Result}; +use crate::error::Result; use crate::PubkyClient; const TEST_RELAY: &str = "http://localhost:15411/pkarr"; +// TODO: Add an in memory cache of packets + impl PubkyClient { //TODO: Allow multiple relays in parallel //TODO: migrate to pkarr::PkarrRelayClient diff --git a/pubky/src/wasm/session.rs b/pubky/src/wasm/session.rs new file mode 100644 index 0000000..ec2e8ca --- /dev/null +++ b/pubky/src/wasm/session.rs @@ -0,0 +1,6 @@ +use pubky_common::session; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Session(pub(crate) session::Session); From 9bef331da018d3e3fd8fecd0a5b8051f42684da3 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 09:22:59 +0300 Subject: [PATCH 023/125] chore(pubky): slight size optimization for wasm bundle size --- Cargo.toml | 4 ++++ pubky/Cargo.toml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 85e44a1..9e2e527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,7 @@ members = [ "pubky","pubky-*"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" + +[profile.release] +lto = true +opt-level = 'z' diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 09d2a44..d71c0bb 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -47,3 +47,6 @@ tokio = "1.37.0" [package.metadata.docs.rs] all-features = true + +# [package.metadata.wasm-pack.profile.release] +# wasm-opt = ['-g', '-O'] From d64f6101028fa401a65876bd5de854a6e9bf3de5 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 09:27:53 +0300 Subject: [PATCH 024/125] feat(pubky): cookie jar for nodejs --- pubky/pkg/package.json | 9 ++++----- pubky/src/lib.rs | 9 +++++++++ pubky/src/native.rs | 5 ++++- pubky/src/shared/auth.rs | 14 +++++++++++--- pubky/src/wasm.rs | 42 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 6532b95..e633cc3 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,18 +2,18 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.0", + "version": "0.0.1", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/pubky/pubky" }, "scripts": { - "lint": "standard --fix", "test": "tape test/*.js -cov", "test-browser": "browserify test/*.js -p esmify | npx tape-run", - "preinstall": "cargo run --bin bundle_pubky_npm", - "prepublishOnly": "npm run lint && npm run test" + "build": "cargo run --bin bundle_pubky_npm", + "preinstall": "npm run build", + "prepublishOnly": "npm run build && npm run test && npm run test-browser" }, "files": [ "nodejs/*", @@ -36,7 +36,6 @@ "devDependencies": { "browser-resolve": "^2.0.0", "esmify": "^2.1.1", - "standard": "^17.1.0", "tape": "^5.8.1", "tape-run": "^11.0.0" } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 48bcb81..72f2df5 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -8,6 +8,12 @@ mod native; #[cfg(target_arch = "wasm32")] mod wasm; +#[cfg(target_arch = "wasm32")] +use std::{ + collections::HashSet, + sync::{Arc, RwLock}, +}; + use wasm_bindgen::prelude::*; #[cfg(not(target_arch = "wasm32"))] @@ -21,4 +27,7 @@ pub struct PubkyClient { http: reqwest::Client, #[cfg(not(target_arch = "wasm32"))] pub(crate) pkarr: PkarrClientAsync, + /// A cookie jar for nodejs fetch. + #[cfg(target_arch = "wasm32")] + pub(crate) session_cookies: Arc>>, } diff --git a/pubky/src/native.rs b/pubky/src/native.rs index fa2ae3f..e6627cc 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -8,7 +8,7 @@ use ::pkarr::{ }; use pkarr::Keypair; use pubky_common::session::Session; -use reqwest::{Method, RequestBuilder}; +use reqwest::{Method, RequestBuilder, Response}; use url::Url; use crate::{error::Result, PubkyClient}; @@ -94,4 +94,7 @@ impl PubkyClient { pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { self.http.request(method, url) } + + pub(crate) fn store_session(&self, response: Response) {} + pub(crate) fn remove_session(&self, pubky: &PublicKey) {} } diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index e78f84a..276ab1d 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -20,15 +20,19 @@ impl PubkyClient { ) -> Result<()> { let homeserver = homeserver.to_string(); + let public_key = &keypair.public_key(); + let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; - url.set_path(&format!("/{}", keypair.public_key())); + url.set_path(&format!("/{}", public_key)); let body = AuthnSignature::generate(keypair, &audience) .as_bytes() .to_owned(); - self.request(Method::PUT, url).body(body).send().await?; + let response = self.request(Method::PUT, url).body(body).send().await?; + + self.store_session(response); self.publish_pubky_homeserver(keypair, &homeserver).await?; @@ -67,6 +71,8 @@ impl PubkyClient { self.request(Method::DELETE, url).send().await?; + self.remove_session(pubky); + Ok(()) } @@ -82,7 +88,9 @@ impl PubkyClient { .as_bytes() .to_owned(); - self.request(Method::POST, url).body(body).send().await?; + let response = self.request(Method::POST, url).body(body).send().await?; + + self.store_session(response); Ok(()) } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 01fcf73..e572670 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,6 +1,11 @@ +use std::{ + collections::HashSet, + sync::{Arc, RwLock}, +}; + use wasm_bindgen::prelude::*; -use reqwest::{Method, RequestBuilder}; +use reqwest::{Method, RequestBuilder, Response}; use url::Url; use crate::PubkyClient; @@ -18,6 +23,7 @@ impl PubkyClient { pub fn new() -> Self { Self { http: reqwest::Client::builder().build().unwrap(), + session_cookies: Arc::new(RwLock::new(HashSet::new())), } } @@ -60,7 +66,37 @@ impl PubkyClient { .map_err(|e| e.into()) } - pub(crate) fn request(&self, method: Method, url: Url) -> reqwest::RequestBuilder { - self.http.request(method, url).fetch_credentials_include() + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { + let request = self.http.request(method, url).fetch_credentials_include(); + + for cookie in self.session_cookies.read().unwrap().iter() { + return request.header("Cookie", cookie); + } + + request + } + + // Support cookies for nodejs + + pub(crate) fn store_session(&self, response: Response) { + if let Some(cookie) = response + .headers() + .get("set-cookie") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.split(';').next()) + { + self.session_cookies + .write() + .unwrap() + .insert(cookie.to_string()); + } + } + pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { + let key = pubky.to_string(); + + self.session_cookies + .write() + .unwrap() + .retain(|cookie| !cookie.starts_with(&key)); } } From ce2a00f020654fc0520ff48793954c97eca12f24 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 09:35:56 +0300 Subject: [PATCH 025/125] chore(pubky): add script to run testnet homeserver --- pubky/pkg/README.md | 9 ++++++--- pubky/pkg/package.json | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index d965c2f..961c93f 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -23,6 +23,9 @@ let keypair = Keypair.random(); let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); await client.signup(keypair, homeserver) + +// Verify that you are signed in. +const session = await client.session(publicKey) ``` ## Test and Development @@ -39,11 +42,11 @@ Clone the Pubky repository: ```bash git clone https://github.com/pubky/pubky -cd pubky/ +cd pubky/pkg ``` -Run the testnet server +Run the local testnet server ```bash -cargo run --bin pubky_homeserver -- --testnet +npm run testnet ``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index e633cc3..ef92f52 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -9,11 +9,13 @@ "url": "https://github.com/pubky/pubky" }, "scripts": { - "test": "tape test/*.js -cov", + "testnet": "cargo run -p pubky_homeserver -- --testnet", + "test": "npm run test-nodejs && npm run test-browser", + "test-nodejs": "tape test/*.js -cov", "test-browser": "browserify test/*.js -p esmify | npx tape-run", "build": "cargo run --bin bundle_pubky_npm", "preinstall": "npm run build", - "prepublishOnly": "npm run build && npm run test && npm run test-browser" + "prepublishOnly": "npm run build && npm run test" }, "files": [ "nodejs/*", From bd5b44e54454a3e7812e0cadd9bdbeaf694f33a0 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 13:13:56 +0300 Subject: [PATCH 026/125] feat(pubky): add put/get methods for js --- pubky/Cargo.toml | 13 +----- pubky/pkg/test/auth.js | 2 +- pubky/pkg/test/public.js | 46 ++++++++++++++++++++ pubky/src/native.rs | 27 +++++++++++- pubky/src/shared/mod.rs | 1 + pubky/src/shared/public.rs | 86 ++++++++++++++++++++++++++++++++++++++ pubky/src/wasm.rs | 44 +++++++------------ pubky/src/wasm/http.rs | 42 +++++++++++++++++++ pubky/src/wasm/keys.rs | 3 +- 9 files changed, 220 insertions(+), 44 deletions(-) create mode 100644 pubky/pkg/test/public.js create mode 100644 pubky/src/shared/public.rs create mode 100644 pubky/src/wasm/http.rs diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index d71c0bb..392402e 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -14,30 +14,21 @@ crate-type = ["cdylib", "rlib"] thiserror = "1.0.62" wasm-bindgen = "0.2.92" url = "2.5.2" -reqwest = { version = "0.12.5", features = ["cookies"] } bytes = "1.6.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } -flume = "0.11.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pkarr = { version="2.1.0", features = ["async"] } +reqwest = { version = "0.12.5", features = ["cookies"], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] pkarr = { version = "2.1.0", default-features = false } +reqwest = { version = "0.12.5", default-features = false } -futures = "0.3.29" js-sys = "0.3.69" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" -web-sys = { version = "0.3.69", features = [ - "console", - "Request", - "RequestInit", - "RequestMode", - "Response", - "Window", -] } [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index a660d99..60f9b4c 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -8,7 +8,7 @@ test('seed auth', async (t) => { const keypair = Keypair.random() const publicKey = keypair.public_key() - const homeserver = PublicKey.try_from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') await client.signup(keypair, homeserver) const session = await client.session(publicKey) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js new file mode 100644 index 0000000..b5ae7a9 --- /dev/null +++ b/pubky/pkg/test/public.js @@ -0,0 +1,46 @@ +import test from 'tape' + +import { PubkyClient, Keypair, PublicKey } from '../index.js' + +test('public: put/get', async (t) => { + const client = new PubkyClient(); + + const keypair = Keypair.random(); + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); + await client.signup(keypair, homeserver); + + const publicKey = keypair.public_key(); + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + // PUT public data, by authorized client + await client.put(publicKey, "/pub/example.com/arbitrary", body); + + + // GET public data without signup or signin + { + const client = new PubkyClient(); + + let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + + t.ok(Buffer.from(response).equals(body)) + } + + // // DELETE public data, by authorized client + // await client.delete(publicKey, "/pub/example.com/arbitrary"); + // + // + // // GET public data without signup or signin + // { + // const client = new PubkyClient(); + // + // let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + // + // t.notOk(response) + // } +}) + +test.skip("not found") + +test.skip("unauthorized") diff --git a/pubky/src/native.rs b/pubky/src/native.rs index e6627cc..b350892 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,11 +1,10 @@ -pub mod public; - use std::time::Duration; use ::pkarr::{ mainline::dht::{DhtSettings, Testnet}, PkarrClient, PublicKey, Settings, SignedPacket, }; +use bytes::Bytes; use pkarr::Keypair; use pubky_common::session::Session; use reqwest::{Method, RequestBuilder, Response}; @@ -21,6 +20,8 @@ impl Default for PubkyClient { } } +// === Public API === + impl PubkyClient { pub fn new() -> Self { Self { @@ -54,6 +55,8 @@ impl PubkyClient { } } + // === Auth === + /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key @@ -80,6 +83,24 @@ impl PubkyClient { self.inner_signin(keypair).await } + // === Public data === + + /// Upload a small payload to a given path. + pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + self.inner_put(pubky, path, content).await + } + + /// Download a small payload from a given path relative to a pubky author. + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + self.inner_get(pubky, path).await + } +} + +// === Internals === + +impl PubkyClient { + // === Pkarr === + pub(crate) async fn pkarr_resolve( &self, public_key: &PublicKey, @@ -91,6 +112,8 @@ impl PubkyClient { Ok(self.pkarr.publish(signed_packet).await?) } + // === HTTP === + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { self.http.request(method, url) } diff --git a/pubky/src/shared/mod.rs b/pubky/src/shared/mod.rs index c61bbfe..ec9bd27 100644 --- a/pubky/src/shared/mod.rs +++ b/pubky/src/shared/mod.rs @@ -1,2 +1,3 @@ pub mod auth; pub mod pkarr; +pub mod public; diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs new file mode 100644 index 0000000..ed2da36 --- /dev/null +++ b/pubky/src/shared/public.rs @@ -0,0 +1,86 @@ +use bytes::Bytes; + +use pkarr::PublicKey; +use reqwest::Method; + +use crate::{error::Result, PubkyClient}; + +impl PubkyClient { + pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{pubky}/{path}")); + + self.request(Method::PUT, url) + .body(content.to_owned()) + .send() + .await?; + + Ok(()) + } + + pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{pubky}/{path}")); + + let response = self.request(Method::GET, url).send().await?; + + // TODO: bail on too large files. + let bytes = response.bytes().await?; + + Ok(bytes) + } +} + +fn normalize_path(path: &str) -> String { + let mut path = path.to_string(); + + if path.starts_with('/') { + path = path[1..].to_string() + } + + // TODO: should we return error instead? + if path.ends_with('/') { + path = path[..path.len()].to_string() + } + + path +} + +#[cfg(test)] +mod tests { + + use crate::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn put_get() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + client + .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) + .await + .unwrap(); + + let response = client + .get(&keypair.public_key(), "/pub/foo.txt") + .await + .unwrap(); + + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) + } +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index e572670..cdff018 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -10,6 +10,7 @@ use url::Url; use crate::PubkyClient; +mod http; mod keys; mod pkarr; mod session; @@ -66,37 +67,22 @@ impl PubkyClient { .map_err(|e| e.into()) } - pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { - let request = self.http.request(method, url).fetch_credentials_include(); + // === Public data === - for cookie in self.session_cookies.read().unwrap().iter() { - return request.header("Cookie", cookie); - } - - request + #[wasm_bindgen] + /// Upload a small payload to a given path. + pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<(), JsValue> { + self.inner_put(pubky.as_inner(), path, content) + .await + .map_err(|e| e.into()) } - // Support cookies for nodejs - - pub(crate) fn store_session(&self, response: Response) { - if let Some(cookie) = response - .headers() - .get("set-cookie") - .and_then(|h| h.to_str().ok()) - .and_then(|s| s.split(';').next()) - { - self.session_cookies - .write() - .unwrap() - .insert(cookie.to_string()); - } - } - pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { - let key = pubky.to_string(); - - self.session_cookies - .write() - .unwrap() - .retain(|cookie| !cookie.starts_with(&key)); + #[wasm_bindgen] + /// Download a small payload from a given path relative to a pubky author. + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + self.inner_get(pubky.as_inner(), path) + .await + .map(|b| (*b).into()) + .map_err(|e| e.into()) } } diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs new file mode 100644 index 0000000..52a0c9d --- /dev/null +++ b/pubky/src/wasm/http.rs @@ -0,0 +1,42 @@ +use crate::PubkyClient; + +use reqwest::{Method, RequestBuilder, Response}; +use url::Url; + +use ::pkarr::PublicKey; + +impl PubkyClient { + pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { + let request = self.http.request(method, url).fetch_credentials_include(); + + for cookie in self.session_cookies.read().unwrap().iter() { + return request.header("Cookie", cookie); + } + + request + } + + // Support cookies for nodejs + + pub(crate) fn store_session(&self, response: Response) { + if let Some(cookie) = response + .headers() + .get("set-cookie") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.split(';').next()) + { + self.session_cookies + .write() + .unwrap() + .insert(cookie.to_string()); + } + } + pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { + let key = pubky.to_string(); + + self.session_cookies + .write() + .unwrap() + .retain(|cookie| !cookie.starts_with(&key)); + } +} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index cbd6ec7..b5043bb 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -51,7 +51,8 @@ impl PublicKey { self.0.to_string() } - #[wasm_bindgen] + #[wasm_bindgen(js_name = "from")] + /// @throws pub fn try_from(value: JsValue) -> Result { let string = value.as_string().ok_or(Error::Generic( "Couldn't create a PublicKey from this type of value".to_string(), From 65f94045b9d58d55e6f5916621ea12a9191e1c04 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:10:58 +0300 Subject: [PATCH 027/125] fix(pubky): cookies jar in nodejs --- pubky/src/wasm/http.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs index 52a0c9d..d89d4ce 100644 --- a/pubky/src/wasm/http.rs +++ b/pubky/src/wasm/http.rs @@ -7,10 +7,10 @@ use ::pkarr::PublicKey; impl PubkyClient { pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { - let request = self.http.request(method, url).fetch_credentials_include(); + let mut request = self.http.request(method, url).fetch_credentials_include(); for cookie in self.session_cookies.read().unwrap().iter() { - return request.header("Cookie", cookie); + request = request.header("Cookie", cookie); } request From bdd07f579ce4694a9b522fcd9f607ca0a458f335 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:11:19 +0300 Subject: [PATCH 028/125] fix(pubky): cookies jar in nodejs --- pubky/src/native/public.rs | 82 -------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 pubky/src/native/public.rs diff --git a/pubky/src/native/public.rs b/pubky/src/native/public.rs deleted file mode 100644 index 9b2a271..0000000 --- a/pubky/src/native/public.rs +++ /dev/null @@ -1,82 +0,0 @@ -use bytes::Bytes; - -use pkarr::PublicKey; - -use crate::{error::Result, PubkyClient}; - -impl PubkyClient { - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let path = normalize_path(path); - - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{pubky}/{path}")); - - self.http.put(url).body(content.to_owned()).send().await?; - - Ok(()) - } - - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { - let path = normalize_path(path); - - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{pubky}/{path}")); - - let response = self.http.get(url).send().await?; - - // TODO: bail on too large files. - let bytes = response.bytes().await?; - - Ok(bytes) - } -} - -fn normalize_path(path: &str) -> String { - let mut path = path.to_string(); - - if path.starts_with('/') { - path = path[1..].to_string() - } - - // TODO: should we return error instead? - if path.ends_with('/') { - path = path[..path.len()].to_string() - } - - path -} - -#[cfg(test)] -mod tests { - - use crate::*; - - use pkarr::{mainline::Testnet, Keypair}; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn put_get() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - let client = PubkyClient::test(&testnet); - - let keypair = Keypair::random(); - - client.signup(&keypair, &server.public_key()).await.unwrap(); - - client - .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await - .unwrap(); - - let response = client - .get(&keypair.public_key(), "/pub/foo.txt") - .await - .unwrap(); - - assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) - } -} From 5b9a49898bb1e5245381f77458a29108a6066990 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:18:45 +0300 Subject: [PATCH 029/125] chore(pubky): clippy issues --- Cargo.lock | 403 +------------------------------------ pubky/pkg/test/keys.js | 2 +- pubky/src/native.rs | 7 +- pubky/src/shared/public.rs | 64 ++++-- pubky/src/wasm.rs | 16 +- pubky/src/wasm/keys.rs | 5 +- 6 files changed, 74 insertions(+), 423 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbeea1b..85d39a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,12 +113,6 @@ dependencies = [ "critical-section", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.3.0" @@ -266,12 +260,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.6.0" @@ -425,22 +413,6 @@ dependencies = [ "url", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - [[package]] name = "cpufeatures" version = "0.2.12" @@ -628,37 +600,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "fastrand" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -683,21 +624,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -825,25 +751,6 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" -[[package]] -name = "h2" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hash32" version = "0.2.1" @@ -853,12 +760,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "headers" version = "0.4.0" @@ -915,7 +816,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bc30da4a93ff8cb98e535d595d6de42731d4719d707bc1c86f579158751a24e" dependencies = [ - "bitflags 2.6.0", + "bitflags", "byteorder", "heed-traits", "heed-types", @@ -1008,7 +909,6 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", "http", "http-body", "httparse", @@ -1020,39 +920,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.6" @@ -1093,16 +960,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -1148,16 +1005,10 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags", "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - [[package]] name = "litrs" version = "0.4.1" @@ -1273,23 +1124,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1331,50 +1165,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "openssl" -version = "0.10.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "overload" version = "0.1.1" @@ -1530,12 +1320,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - [[package]] name = "postcard" version = "1.0.8" @@ -1580,8 +1364,6 @@ name = "pubky" version = "0.1.0" dependencies = [ "bytes", - "flume", - "futures", "js-sys", "pkarr", "pubky-common", @@ -1592,7 +1374,6 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "web-sys", ] [[package]] @@ -1691,7 +1472,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -1759,33 +1540,25 @@ dependencies = [ "bytes", "cookie", "cookie_store", - "encoding_rs", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", - "hyper-rustls", - "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", - "system-configuration", "tokio", - "tokio-native-tls", "tower-service", "url", "wasm-bindgen", @@ -1794,21 +1567,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1824,59 +1582,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.6.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" -dependencies = [ - "base64 0.22.1", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" - -[[package]] -name = "rustls-webpki" -version = "0.102.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.17" @@ -1889,44 +1594,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "self_cell" version = "1.0.4" @@ -2072,7 +1745,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -2175,39 +1848,6 @@ dependencies = [ "crossbeam-queue", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" -dependencies = [ - "cfg-if", - "fastrand", - "rustix", - "windows-sys 0.52.0", -] - [[package]] name = "thiserror" version = "1.0.62" @@ -2314,27 +1954,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.11" @@ -2387,7 +2006,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.6.0", + "bitflags", "bytes", "http", "http-body", @@ -2505,12 +2124,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.2" @@ -2534,12 +2147,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.4" diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index 76e3202..306e21e 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -9,5 +9,5 @@ test('generate keys from a seed', async (t) => { const publicKey = keypair.public_key() - t.is(publicKey.to_string(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') + t.is(publicKey.z32(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') }) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index b350892..5805241 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -91,9 +91,14 @@ impl PubkyClient { } /// Download a small payload from a given path relative to a pubky author. - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result> { self.inner_get(pubky, path).await } + + // /// Delete a file at a path relative to a pubky author. + // pub async fn delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { + // self.inner_delete(pubky, path).await + // } } // === Internals === diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index ed2da36..2b1edaa 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -1,17 +1,14 @@ use bytes::Bytes; use pkarr::PublicKey; -use reqwest::Method; +use reqwest::{Method, Response, StatusCode}; +use url::Url; use crate::{error::Result, PubkyClient}; impl PubkyClient { pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let path = normalize_path(path); - - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{pubky}/{path}")); + let url = self.url(pubky, path).await?; self.request(Method::PUT, url) .body(content.to_owned()) @@ -21,23 +18,41 @@ impl PubkyClient { Ok(()) } - pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result { - let path = normalize_path(path); + pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result> { + let url = self.url(pubky, path).await?; + + let res = self.request(Method::GET, url).send().await?; + + if res.status() == StatusCode::NOT_FOUND { + return Ok(None); + } + + // TODO: bail on too large files. + let bytes = res.bytes().await?; + + Ok(Some(bytes)) + } + + pub async fn inner_delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { + let url = self.url(pubky, path).await?; + + self.request(Method::DELETE, url).send().await?; + + Ok(()) + } + + async fn url(&self, pubky: &PublicKey, path: &str) -> Result { + let path = normalize_path(path)?; let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{pubky}/{path}")); - let response = self.request(Method::GET, url).send().await?; - - // TODO: bail on too large files. - let bytes = response.bytes().await?; - - Ok(bytes) + Ok(url) } } -fn normalize_path(path: &str) -> String { +fn normalize_path(path: &str) -> Result { let mut path = path.to_string(); if path.starts_with('/') { @@ -49,7 +64,7 @@ fn normalize_path(path: &str) -> String { path = path[..path.len()].to_string() } - path + Ok(path) } #[cfg(test)] @@ -61,7 +76,7 @@ mod tests { use pubky_homeserver::Homeserver; #[tokio::test] - async fn put_get() { + async fn put_get_delete() { let testnet = Testnet::new(3); let server = Homeserver::start_test(&testnet).await.unwrap(); @@ -79,8 +94,21 @@ mod tests { let response = client .get(&keypair.public_key(), "/pub/foo.txt") .await + .unwrap() .unwrap(); - assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); + + // client + // .delete(&keypair.public_key(), "/pub/foo.txt") + // .await + // .unwrap(); + // + // let response = client + // .get(&keypair.public_key(), "/pub/foo.txt") + // .await + // .unwrap(); + // + // assert_eq!(response, None); } } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index cdff018..7cadbbd 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -18,6 +18,12 @@ mod session; use keys::{Keypair, PublicKey}; use session::Session; +impl Default for PubkyClient { + fn default() -> Self { + Self::new() + } +} + #[wasm_bindgen] impl PubkyClient { #[wasm_bindgen(constructor)] @@ -47,7 +53,7 @@ impl PubkyClient { pub async fn session(&self, pubky: &PublicKey) -> Result, JsValue> { self.inner_session(pubky.as_inner()) .await - .map(|s| s.map(|s| Session(s).into())) + .map(|s| s.map(Session)) .map_err(|e| e.into()) } @@ -79,10 +85,14 @@ impl PubkyClient { #[wasm_bindgen] /// Download a small payload from a given path relative to a pubky author. - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + pub async fn get( + &self, + pubky: &PublicKey, + path: &str, + ) -> Result, JsValue> { self.inner_get(pubky.as_inner(), path) .await - .map(|b| (*b).into()) + .map(|b| b.map(|b| (&*b).into())) .map_err(|e| e.into()) } } diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index b5043bb..d1ef078 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -47,7 +47,8 @@ impl PublicKey { } #[wasm_bindgen] - pub fn to_string(&self) -> String { + /// Returns the z-base32 encoding of this public key + pub fn z32(&self) -> String { self.0.to_string() } @@ -59,7 +60,7 @@ impl PublicKey { ))?; Ok(PublicKey( - pkarr::PublicKey::try_from(string).map_err(|e| Error::Pkarr(e))?, + pkarr::PublicKey::try_from(string).map_err(Error::Pkarr)?, )) } } From 75d8acde65daf6c0d97d4bb0b2f42d7b53567176 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:48:51 +0300 Subject: [PATCH 030/125] feat(pubky): override default pkarr relays --- pubky/pkg/README.md | 28 ++++++++++++++++++++++++++-- pubky/pkg/package.json | 2 +- pubky/pkg/test/auth.js | 10 +++++----- pubky/pkg/test/public.js | 4 ++-- pubky/src/lib.rs | 2 ++ pubky/src/wasm.rs | 26 ++++++++++++++++++++++++++ pubky/src/wasm/pkarr.rs | 12 +++++++----- 7 files changed, 69 insertions(+), 15 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 961c93f..784834a 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -11,7 +11,7 @@ npm install @synonymdev/pubky ## Getting started ```js -import PubkyClient from "@synonymdev/pubky"; +import { PubkyClient, Keypair, PublicKey } from '../index.js' // Initialize PubkyClient with Pkarr relay(s). let client = new PubkyClient(); @@ -20,12 +20,26 @@ let client = new PubkyClient(); let keypair = Keypair.random(); // Create a new account -let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); +let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); await client.signup(keypair, homeserver) // Verify that you are signed in. const session = await client.session(publicKey) + +const publicKey = keypair.public_key(); + +const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + +// PUT public data, by authorized client +await client.put(publicKey, "/pub/example.com/arbitrary", body); + +// GET public data without signup or signin +{ + const client = new PubkyClient(); + + let response = await client.get(publicKey, "/pub/example.com/arbitrary"); +} ``` ## Test and Development @@ -50,3 +64,13 @@ Run the local testnet server ```bash npm run testnet ``` + +Pass the logged addresses as inputs to `PubkyClient` + +```js +import { PubkyClient, PublicKey } from '../index.js' + +const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]); + +let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); +``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index ef92f52..0adc9b9 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", "repository": { "type": "git", diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 60f9b4c..cd54e06 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -2,8 +2,8 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' -test('seed auth', async (t) => { - const client = new PubkyClient() +test('auth', async (t) => { + const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) const keypair = Keypair.random() const publicKey = keypair.public_key() @@ -12,19 +12,19 @@ test('seed auth', async (t) => { await client.signup(keypair, homeserver) const session = await client.session(publicKey) - t.ok(session) + t.ok(session, "signup") { await client.signout(publicKey) const session = await client.session(publicKey) - t.notOk(session) + t.notOk(session, "singout") } { await client.signin(keypair) const session = await client.session(publicKey) - t.ok(session) + t.ok(session, "signin") } }) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index b5ae7a9..ddf02a4 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -3,7 +3,7 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' test('public: put/get', async (t) => { - const client = new PubkyClient(); + const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) const keypair = Keypair.random(); @@ -20,7 +20,7 @@ test('public: put/get', async (t) => { // GET public data without signup or signin { - const client = new PubkyClient(); + const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) let response = await client.get(publicKey, "/pub/example.com/arbitrary"); diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 72f2df5..ab6732b 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -30,4 +30,6 @@ pub struct PubkyClient { /// A cookie jar for nodejs fetch. #[cfg(target_arch = "wasm32")] pub(crate) session_cookies: Arc>>, + #[cfg(target_arch = "wasm32")] + pub(crate) pkarr_relays: Vec, } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 7cadbbd..0607b75 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -24,6 +24,8 @@ impl Default for PubkyClient { } } +static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"]; + #[wasm_bindgen] impl PubkyClient { #[wasm_bindgen(constructor)] @@ -31,9 +33,33 @@ impl PubkyClient { Self { http: reqwest::Client::builder().build().unwrap(), session_cookies: Arc::new(RwLock::new(HashSet::new())), + pkarr_relays: DEFAULT_RELAYS.into_iter().map(|s| s.to_string()).collect(), } } + /// Set the relays used for publishing and resolving Pkarr packets. + /// + /// By default, [PubkyClient] will use `["https://relay.pkarr.org"]` + #[wasm_bindgen(js_name = "setPkarrRelays")] + pub fn set_pkarr_relays(mut self, relays: Vec) -> Self { + let relays: Vec = relays + .into_iter() + .filter_map(|name| name.as_string()) + .collect(); + + self.pkarr_relays = relays; + self + } + + #[wasm_bindgen(js_name = "getPkarrRelays")] + pub fn get_pkarr_relays(&self) -> Vec { + self.pkarr_relays + .clone() + .into_iter() + .map(JsValue::from) + .collect() + } + /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 8b43299..49726f6 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -5,20 +5,20 @@ pub use pkarr::{PublicKey, SignedPacket}; use crate::error::Result; use crate::PubkyClient; -const TEST_RELAY: &str = "http://localhost:15411/pkarr"; - // TODO: Add an in memory cache of packets impl PubkyClient { - //TODO: Allow multiple relays in parallel //TODO: migrate to pkarr::PkarrRelayClient pub(crate) async fn pkarr_resolve( &self, public_key: &PublicKey, ) -> Result> { + //TODO: Allow multiple relays in parallel + let relay = self.pkarr_relays.first().expect("initialized with relays"); + let res = self .http - .get(format!("{TEST_RELAY}/{}", public_key)) + .get(format!("{relay}/{}", public_key)) .send() .await?; @@ -35,8 +35,10 @@ impl PubkyClient { } pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + let relay = self.pkarr_relays.first().expect("initialized with relays"); + self.http - .put(format!("{TEST_RELAY}/{}", signed_packet.public_key())) + .put(format!("{relay}/{}", signed_packet.public_key())) .body(signed_packet.to_relay_payload()) .send() .await?; From 59839c521d33996608a63291c5056dd34cd3e305 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:55:23 +0300 Subject: [PATCH 031/125] docs: fix some comments --- pubky-homeserver/src/config.rs | 2 -- pubky/src/native.rs | 4 ++-- pubky/src/wasm.rs | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 6949b09..2136e0c 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -11,8 +11,6 @@ const DEFAULT_HOMESERVER_PORT: u16 = 6287; const DEFAULT_STORAGE_DIR: &str = "pubky"; /// Server configuration -/// -/// The config is usually loaded from a file with [`Self::load`]. #[derive( // Serialize, Deserialize, Clone, diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 5805241..b63c844 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -67,8 +67,8 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. + /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), + /// or [reqwest::Error] if the response has any other `>=400` status code. pub async fn session(&self, pubky: &PublicKey) -> Result> { self.inner_session(pubky).await } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 0607b75..b04de3b 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -73,8 +73,8 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. + /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), + /// or throws the recieved error if the response has any other `>=400` status code. #[wasm_bindgen] pub async fn session(&self, pubky: &PublicKey) -> Result, JsValue> { self.inner_session(pubky.as_inner()) From 2493b0ef5ab824265ae08e1992153409daf2ca44 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 15:28:20 +0300 Subject: [PATCH 032/125] docs: fix getting started example in npm package --- pubky/pkg/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 784834a..e3aa3f5 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -24,11 +24,11 @@ let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn4 await client.signup(keypair, homeserver) +const publicKey = keypair.public_key(); + // Verify that you are signed in. const session = await client.session(publicKey) -const publicKey = keypair.public_key(); - const body = Buffer.from(JSON.stringify({ foo: 'bar' })) // PUT public data, by authorized client From 5b45863d34d3dec09bd38b9726f3c9b82ee3191d Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 15:28:20 +0300 Subject: [PATCH 033/125] docs: fix getting started example in npm package --- pubky/pkg/README.md | 4 ++-- pubky/pkg/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 784834a..e3aa3f5 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -24,11 +24,11 @@ let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn4 await client.signup(keypair, homeserver) +const publicKey = keypair.public_key(); + // Verify that you are signed in. const session = await client.session(publicKey) -const publicKey = keypair.public_key(); - const body = Buffer.from(JSON.stringify({ foo: 'bar' })) // PUT public data, by authorized client diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 0adc9b9..0c66d29 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "repository": { "type": "git", From dcf6bdf3dab119db41ecd482bfdda3a23e05d664 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 30 Jul 2024 11:00:24 +0300 Subject: [PATCH 034/125] fix(homeserver): handle missing user agent --- pubky-homeserver/src/routes/auth.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 60761fb..7feafbb 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -30,7 +30,7 @@ use crate::{ #[debug_handler] pub async fn signup( State(state): State, - TypedHeader(user_agent): TypedHeader, + user_agent: Option>, cookies: Cookies, pubky: Pubky, uri: Uri, @@ -38,15 +38,7 @@ pub async fn signup( ) -> Result { // TODO: Verify invitation link. // TODO: add errors in case of already axisting user. - signin( - State(state), - TypedHeader(user_agent), - cookies, - pubky, - uri, - body, - ) - .await + signin(State(state), user_agent, cookies, pubky, uri, body).await } pub async fn session( @@ -104,7 +96,7 @@ pub async fn signout( pub async fn signin( State(state): State, - TypedHeader(user_agent): TypedHeader, + user_agent: Option>, cookies: Cookies, pubky: Pubky, uri: Uri, @@ -141,10 +133,11 @@ pub async fn signin( .open_database(&wtxn, Some(SESSIONS_TABLE))? .expect("Sessions table already created"); - // TODO: handle not having a user agent? let mut session = Session::new(); - session.set_user_agent(user_agent.to_string()); + if let Some(user_agent) = user_agent { + session.set_user_agent(user_agent.to_string()); + } sessions.put(&mut wtxn, &session_secret, &session.serialize())?; From d8d3a43abdc8b6038fb169da9e211b2a61914cb7 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 30 Jul 2024 13:46:04 +0300 Subject: [PATCH 035/125] chore: fix tests in ci --- .github/workflows/rust.yaml | 24 +++++++++++++----------- pubky-homeserver/src/config.rs | 33 +++++++++++++++++++++++++-------- pubky-homeserver/src/main.rs | 17 +++++------------ pubky-homeserver/src/server.rs | 1 - pubky/src/native.rs | 2 +- pubky/src/shared/pkarr.rs | 8 +------- 6 files changed, 45 insertions(+), 40 deletions(-) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index eca9a84..85357b4 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -36,6 +36,7 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-index- - name: Cache cargo build + id: cargo-build-cache uses: actions/cache@v2 with: path: target @@ -54,20 +55,21 @@ jobs: cargo install cargo-nextest fi - # - name: Check no default features - # run: cargo check --no-default-features + - name: Check no default features + run: cargo check --no-default-features - # - name: Check formatting - # run: cargo fmt -- --check + - name: Check formatting + run: cargo fmt -- --check - # - name: Lint with Clippy - # run: cargo clippy --workspace --all-features --bins --tests + - name: Lint with Clippy + run: cargo clippy --workspace --all-features --bins --tests - # - name: Build - # run: cargo build --release --workspace --all-features --verbose + - name: Build + if: steps.cargo-build-cache.outputs.cache-hit != 'true' + run: cargo build --release --workspace --all-features --verbose - name: Run tests with Nextest - run: cargo nextest run homeserver_in_tokio + run: cargo nextest run --all-features --workspace --verbose - # - name: Run docs - # run: cargo doc --workspace --all-features --no-deps --document-private-items --verbose + - name: Run docs + run: cargo doc --workspace --all-features --no-deps --document-private-items --verbose diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 2136e0c..25fbc66 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -16,15 +16,14 @@ const DEFAULT_STORAGE_DIR: &str = "pubky"; Clone, )] pub struct Config { - pub port: Option, - pub bootstrap: Option>, - pub domain: String, + port: Option, + bootstrap: Option>, + domain: String, /// Path to the storage directory /// /// Defaults to a directory in the OS data directory - pub storage: Option, - pub keypair: Keypair, - pub request_timeout: Option, + storage: Option, + keypair: Keypair, } impl Config { @@ -37,6 +36,26 @@ impl Config { // Ok(config) // } + /// Testnet configurations + pub fn testnet() -> Self { + let testnet = pkarr::mainline::Testnet::new(10); + + let bootstrap = Some(testnet.bootstrap.to_owned()); + let storage = Some( + std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join(DEFAULT_STORAGE_DIR), + ); + + Self { + bootstrap, + storage, + port: Some(15411), + keypair: Keypair::from_secret_key(&[0_u8; 32]), + ..Default::default() + } + } + /// Test configurations pub fn test(testnet: &pkarr::mainline::Testnet) -> Self { let bootstrap = Some(testnet.bootstrap.to_owned()); @@ -49,7 +68,6 @@ impl Config { Self { bootstrap, storage, - request_timeout: Some(Duration::from_millis(10)), ..Default::default() } } @@ -93,7 +111,6 @@ impl Default for Config { domain: "localhost".to_string(), storage: None, keypair: Keypair::random(), - request_timeout: None, } } } diff --git a/pubky-homeserver/src/main.rs b/pubky-homeserver/src/main.rs index e45cf3d..2a17bda 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use pkarr::{mainline::Testnet, Keypair}; use pubky_homeserver::{config::Config, Homeserver}; use clap::Parser; @@ -24,18 +23,12 @@ async fn main() -> Result<()> { ) .init(); - let server = if args.testnet { - let testnet = Testnet::new(10); - - Homeserver::start(Config { - port: Some(15411), - keypair: Keypair::from_secret_key(&[0_u8; 32]), - ..Config::test(&testnet) - }) - .await? + let server = Homeserver::start(if args.testnet { + Config::testnet() } else { - Homeserver::start(Default::default()).await? - }; + Default::default() + }) + .await?; server.run_until_done().await?; diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 12d497c..9837ed9 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -39,7 +39,6 @@ impl Homeserver { let pkarr_client = PkarrClient::new(Settings { dht: DhtSettings { bootstrap: config.bootstsrap(), - request_timeout: config.request_timeout, ..Default::default() }, ..Default::default() diff --git a/pubky/src/native.rs b/pubky/src/native.rs index b63c844..a2afea0 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -44,7 +44,7 @@ impl PubkyClient { .unwrap(), pkarr: PkarrClient::new(Settings { dht: DhtSettings { - request_timeout: Some(Duration::from_millis(10)), + request_timeout: Some(Duration::from_millis(100)), bootstrap: Some(testnet.bootstrap.to_owned()), ..DhtSettings::default() }, diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index f35ce5c..c0a800e 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -139,17 +139,11 @@ mod tests { use pkarr::{ dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, + mainline::{dht::DhtSettings, Dht, Testnet}, Keypair, PkarrClient, Settings, SignedPacket, }; use pubky_homeserver::Homeserver; - #[tokio::test] - fn homeserver_in_tokio() { - let testnet = Testnet::new(10); - let server = Homeserver::start_test(&testnet).await.unwrap(); - } - #[tokio::test] async fn resolve_homeserver() { let testnet = Testnet::new(10); From 7e451a5d007422e4273d77a8d657ed2a84cf65e2 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 30 Jul 2024 15:55:20 +0300 Subject: [PATCH 036/125] feat(pubky): add PubkyClient.testnet() helper --- pubky-homeserver/src/config.rs | 8 ++++++++ pubky-homeserver/src/server.rs | 1 + pubky/pkg/package.json | 2 +- pubky/pkg/test/auth.js | 2 +- pubky/pkg/test/public.js | 4 ++-- pubky/src/wasm.rs | 15 ++++++++++++++- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 25fbc66..22686cf 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -24,6 +24,8 @@ pub struct Config { /// Defaults to a directory in the OS data directory storage: Option, keypair: Keypair, + + dht_request_timeout: Option, } impl Config { @@ -52,6 +54,7 @@ impl Config { storage, port: Some(15411), keypair: Keypair::from_secret_key(&[0_u8; 32]), + dht_request_timeout: Some(Duration::from_millis(10)), ..Default::default() } } @@ -101,6 +104,10 @@ impl Config { pub fn keypair(&self) -> &Keypair { &self.keypair } + + pub(crate) fn dht_request_timeout(&self) -> Option { + self.dht_request_timeout + } } impl Default for Config { @@ -111,6 +118,7 @@ impl Default for Config { domain: "localhost".to_string(), storage: None, keypair: Keypair::random(), + dht_request_timeout: None, } } } diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 9837ed9..2863317 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -39,6 +39,7 @@ impl Homeserver { let pkarr_client = PkarrClient::new(Settings { dht: DhtSettings { bootstrap: config.bootstsrap(), + request_timeout: config.dht_request_timeout(), ..Default::default() }, ..Default::default() diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 0c66d29..ad5c858 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.0.3", + "version": "0.1.1", "license": "MIT", "repository": { "type": "git", diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index cd54e06..d738c99 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -3,7 +3,7 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' test('auth', async (t) => { - const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) + const client = PubkyClient.testnet(); const keypair = Keypair.random() const publicKey = keypair.public_key() diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index ddf02a4..624f7dc 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -3,7 +3,7 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' test('public: put/get', async (t) => { - const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) + const client = PubkyClient.testnet(); const keypair = Keypair.random(); @@ -20,7 +20,7 @@ test('public: put/get', async (t) => { // GET public data without signup or signin { - const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) + const client = PubkyClient.testnet(); let response = await client.get(publicKey, "/pub/example.com/arbitrary"); diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index b04de3b..d22a6ed 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -25,6 +25,7 @@ impl Default for PubkyClient { } static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"]; +static TESTNET_RELAYS: [&str; 1] = ["http://localhost:15411/pkarr"]; #[wasm_bindgen] impl PubkyClient { @@ -37,7 +38,18 @@ impl PubkyClient { } } - /// Set the relays used for publishing and resolving Pkarr packets. + /// Create a client with with configurations appropriate for local testing: + /// - set Pkarr relays to `["http://localhost:15411/pkarr"]` instead of default relay. + #[wasm_bindgen] + pub fn testnet() -> Self { + Self { + http: reqwest::Client::builder().build().unwrap(), + session_cookies: Arc::new(RwLock::new(HashSet::new())), + pkarr_relays: TESTNET_RELAYS.into_iter().map(|s| s.to_string()).collect(), + } + } + + /// Set Pkarr relays used for publishing and resolving Pkarr packets. /// /// By default, [PubkyClient] will use `["https://relay.pkarr.org"]` #[wasm_bindgen(js_name = "setPkarrRelays")] @@ -51,6 +63,7 @@ impl PubkyClient { self } + // Read the set of pkarr relays used by this client. #[wasm_bindgen(js_name = "getPkarrRelays")] pub fn get_pkarr_relays(&self) -> Vec { self.pkarr_relays From e9bb104ba57df6a2d6f2610ce5a14ae8bdbded93 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 30 Jul 2024 15:56:53 +0300 Subject: [PATCH 037/125] fix(pubky): recursive resolution of homeserver --- pubky/src/shared/pkarr.rs | 47 ++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index c0a800e..92f7bc8 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -65,6 +65,7 @@ impl PubkyClient { // TODO: cache the result of this function? let mut target = target.to_string(); + let mut homeserver_public_key = None; let mut host = target.clone(); @@ -72,32 +73,42 @@ impl PubkyClient { // PublicKey is very good at extracting the Pkarr TLD from a string. while let Ok(public_key) = PublicKey::try_from(target.clone()) { + if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION { + break; + }; + step += 1; - let response = self + if let Some(signed_packet) = self .pkarr_resolve(&public_key) .await - .map_err(|_| Error::ResolveEndpoint(original_target.into()))?; + .map_err(|_| Error::ResolveEndpoint(original_target.into()))? + { + // Choose most prior SVCB record + let svcb = signed_packet.resource_records(&target).fold( + None, + |prev: Option, answer| { + if let pkarr::dns::rdata::RData::SVCB(curr) = &answer.rdata { + let curr = curr.clone(); - let mut prior = None; - - if let Some(signed_packet) = response { - for answer in signed_packet.resource_records(&target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) + if curr.priority == 0 { + return Some(curr); + } + if let Some(prev) = &prev { + // TODO return random if priority is the same + if curr.priority >= prev.priority { + return Some(curr); + } + } else { + return Some(curr); } - // TODO return random if priority is the same - } else { - prior = Some(svcb) } - } - } - if let Some(svcb) = prior { + prev + }, + ); + + if let Some(svcb) = svcb { homeserver_public_key = Some(public_key.clone()); target = svcb.target.to_string(); From b3152942dc736d6e868e0757d28525cee53f274f Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 30 Jul 2024 20:28:05 +0300 Subject: [PATCH 038/125] feat(homeserver): return 401 forbidden if you try to write to someone else's drive --- pubky-homeserver/src/database.rs | 61 +------------------ pubky-homeserver/src/database/tables/blobs.rs | 29 +++++++++ .../src/database/tables/entries.rs | 48 ++++++++++++++- .../src/database/tables/sessions.rs | 45 ++++++++++++++ pubky-homeserver/src/routes/public.rs | 27 ++++++-- 5 files changed, 143 insertions(+), 67 deletions(-) diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 5bbe6c2..bbd21b8 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -1,4 +1,5 @@ use std::fs; + use std::path::Path; use bytes::Bytes; @@ -32,66 +33,6 @@ impl DB { Ok(db) } - - pub fn put_entry( - &mut self, - public_key: &PublicKey, - path: &str, - rx: flume::Receiver, - ) -> anyhow::Result<()> { - let mut wtxn = self.env.write_txn()?; - - let mut hasher = Hasher::new(); - let mut bytes = vec![]; - let mut length = 0; - - while let Ok(chunk) = rx.recv() { - hasher.update(&chunk); - bytes.extend_from_slice(&chunk); - length += chunk.len(); - } - - let hash = hasher.finalize(); - - self.tables.blobs.put(&mut wtxn, hash.as_bytes(), &bytes)?; - - let mut entry = Entry::new(); - - entry.set_content_hash(hash); - entry.set_content_length(length); - - let mut key = vec![]; - key.extend_from_slice(public_key.as_bytes()); - key.extend_from_slice(path.as_bytes()); - - self.tables.entries.put(&mut wtxn, &key, &entry.serialize()); - - wtxn.commit()?; - - Ok(()) - } - - pub fn get_blob( - &mut self, - public_key: &PublicKey, - path: &str, - ) -> anyhow::Result> { - let mut rtxn = self.env.read_txn()?; - - let mut key = vec![]; - key.extend_from_slice(public_key.as_bytes()); - key.extend_from_slice(path.as_bytes()); - - if let Some(bytes) = self.tables.entries.get(&rtxn, &key)? { - let entry = Entry::deserialize(bytes)?; - - if let Some(blob) = self.tables.blobs.get(&rtxn, entry.content_hash())? { - return Ok(Some(Bytes::from(blob.to_vec()))); - }; - }; - - Ok(None) - } } #[cfg(test)] diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs index 0148d6f..80c46ae 100644 --- a/pubky-homeserver/src/database/tables/blobs.rs +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -4,8 +4,37 @@ use heed::{ types::{Bytes, Str}, BoxedError, BytesDecode, BytesEncode, Database, }; +use pkarr::PublicKey; + +use crate::database::DB; + +use super::entries::Entry; /// hash of the blob => bytes. pub type BlobsTable = Database; pub const BLOBS_TABLE: &str = "blobs"; + +impl DB { + pub fn get_blob( + &mut self, + public_key: &PublicKey, + path: &str, + ) -> anyhow::Result> { + let mut rtxn = self.env.read_txn()?; + + let mut key = vec![]; + key.extend_from_slice(public_key.as_bytes()); + key.extend_from_slice(path.as_bytes()); + + if let Some(bytes) = self.tables.entries.get(&rtxn, &key)? { + let entry = Entry::deserialize(bytes)?; + + if let Some(blob) = self.tables.blobs.get(&rtxn, entry.content_hash())? { + return Ok(Some(bytes::Bytes::from(blob.to_vec()))); + }; + }; + + Ok(None) + } +} diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 7c9e2e3..a0e461b 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -1,3 +1,4 @@ +use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, time::SystemTime}; @@ -7,13 +8,58 @@ use heed::{ BoxedError, BytesDecode, BytesEncode, Database, }; -use pubky_common::{crypto::Hash, timestamp::Timestamp}; +use pubky_common::{ + crypto::{Hash, Hasher}, + timestamp::Timestamp, +}; + +use crate::database::DB; /// full_path(pubky/*path) => Entry. pub type EntriesTable = Database; pub const ENTRIES_TABLE: &str = "entries"; +impl DB { + pub fn put_entry( + &mut self, + public_key: &PublicKey, + path: &str, + rx: flume::Receiver, + ) -> anyhow::Result<()> { + let mut wtxn = self.env.write_txn()?; + + let mut hasher = Hasher::new(); + let mut bytes = vec![]; + let mut length = 0; + + while let Ok(chunk) = rx.recv() { + hasher.update(&chunk); + bytes.extend_from_slice(&chunk); + length += chunk.len(); + } + + let hash = hasher.finalize(); + + self.tables.blobs.put(&mut wtxn, hash.as_bytes(), &bytes)?; + + let mut entry = Entry::new(); + + entry.set_content_hash(hash); + entry.set_content_length(length); + + let mut key = vec![]; + key.extend_from_slice(public_key.as_bytes()); + key.extend_from_slice(path.as_bytes()); + + self.tables.entries.put(&mut wtxn, &key, &entry.serialize()); + + wtxn.commit()?; + + Ok(()) + } +} + #[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Entry { /// Encoding version diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs index 6e9d8f8..db6c53d 100644 --- a/pubky-homeserver/src/database/tables/sessions.rs +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -4,8 +4,53 @@ use heed::{ types::{Bytes, Str}, BoxedError, BytesDecode, BytesEncode, Database, }; +use pkarr::PublicKey; +use pubky_common::session::Session; +use serde::Deserialize; +use tower_cookies::Cookies; + +use crate::database::DB; /// session secret => Session. pub type SessionsTable = Database; pub const SESSIONS_TABLE: &str = "sessions"; + +impl DB { + pub fn get_session( + &mut self, + cookies: Cookies, + public_key: &PublicKey, + path: &str, + ) -> anyhow::Result> { + if let Some(bytes) = self.get_session_bytes(cookies, public_key, path)? { + return Ok(Some(Session::deserialize(&bytes)?)); + }; + + Ok(None) + } + + pub fn get_session_bytes( + &mut self, + cookies: Cookies, + public_key: &PublicKey, + path: &str, + ) -> anyhow::Result>> { + if let Some(cookie) = cookies.get(&public_key.to_string()) { + let rtxn = self.env.read_txn()?; + + let sessions: SessionsTable = self + .env + .open_database(&rtxn, Some(SESSIONS_TABLE))? + .expect("Session table already created"); + + let session = sessions.get(&rtxn, cookie.value())?.map(|s| s.to_vec()); + + rtxn.commit()?; + + return Ok(session); + }; + + Ok(None) + } +} diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index cb07cee..4a7834c 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -7,6 +7,7 @@ use axum::{ }; use axum_extra::body::AsyncReadBody; use futures_util::stream::StreamExt; +use tower_cookies::Cookies; use tracing::debug; @@ -26,9 +27,27 @@ pub async fn put( State(mut state): State, pubky: Pubky, path: EntryPath, + cookies: Cookies, mut body: Body, ) -> Result { - // TODO: return an error if path does not start with '/pub/' + let public_key = pubky.public_key().clone(); + let path = path.as_str().to_string(); + + // TODO: can we move this logic to the extractor or a layer + // to perform this validation? + let session = state + .db + .get_session(cookies, &public_key, &path)? + .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; + + if !path.starts_with("pub/") { + return Err(Error::new( + StatusCode::FORBIDDEN, + "Writing to directories other than '/pub/' is forbidden".into(), + )); + } + + // TODO: should we forbid paths ending with `/`? let mut stream = body.into_data_stream(); @@ -41,11 +60,7 @@ pub async fn put( // to stream this to filesystem, and keep track of any failed // writes to GC these files later. - let public_key = pubky.public_key(); - - // TODO: Authorize - - state.db.put_entry(public_key, path.as_str(), rx); + state.db.put_entry(&public_key, &path, rx); Ok(()) }); From 7feef474d3b60127c395cb433298ac1e799d1405 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 30 Jul 2024 20:28:34 +0300 Subject: [PATCH 039/125] feat(pubky): return errors if response status is >= 400 --- pubky/src/shared/public.rs | 74 +++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 09e883c..b78554a 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -10,25 +10,30 @@ impl PubkyClient { pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { let url = self.url(pubky, path).await?; - self.request(Method::PUT, url) + let response = self + .request(Method::PUT, url) .body(content.to_owned()) .send() .await?; + response.error_for_status()?; + Ok(()) } pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result> { let url = self.url(pubky, path).await?; - let res = self.request(Method::GET, url).send().await?; + let response = self.request(Method::GET, url).send().await?; - if res.status() == StatusCode::NOT_FOUND { + response.error_for_status_ref()?; + + if response.status() == StatusCode::NOT_FOUND { return Ok(None); } // TODO: bail on too large files. - let bytes = res.bytes().await?; + let bytes = response.bytes().await?; Ok(Some(bytes)) } @@ -36,7 +41,9 @@ impl PubkyClient { pub async fn inner_delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { let url = self.url(pubky, path).await?; - self.request(Method::DELETE, url).send().await?; + let response = self.request(Method::DELETE, url).send().await?; + + response.error_for_status_ref()?; Ok(()) } @@ -70,10 +77,13 @@ fn normalize_path(path: &str) -> Result { #[cfg(test)] mod tests { + use core::panic; + use crate::*; use pkarr::{mainline::Testnet, Keypair}; use pubky_homeserver::Homeserver; + use reqwest::StatusCode; #[tokio::test] async fn put_get_delete() { @@ -111,4 +121,58 @@ mod tests { // // assert_eq!(response, None); } + + #[tokio::test] + async fn forbidden_put_delete() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let public_key = keypair.public_key(); + + let other_client = PubkyClient::test(&testnet); + { + let other = Keypair::random(); + + other_client + .signup(&other, &server.public_key()) + .await + .unwrap(); + + let response = other_client + .put(&public_key, "/pub/foo.txt", &[0, 1, 2, 3, 4]) + .await; + + match response { + Err(Error::Reqwest(error)) => { + assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) + } + error => { + panic!("expected error StatusCode::UNAUTHORIZED") + } + } + } + + // client + // .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) + // .await + // .unwrap(); + // + // client + // .delete(&keypair.public_key(), "/pub/foo.txt") + // .await + // .unwrap(); + // + // let response = client + // .get(&keypair.public_key(), "/pub/foo.txt") + // .await + // .unwrap(); + // + // assert_eq!(response, None); + } } From 65a6d776c379ff465a7da5d48dc836f6dc0b7012 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 31 Jul 2024 17:46:59 +0300 Subject: [PATCH 040/125] feat(pubky): accept url instead of public key and path --- pubky/pkg/README.md | 7 ++-- pubky/pkg/test/public.js | 6 ++-- pubky/src/native.rs | 8 ++--- pubky/src/shared/auth.rs | 26 +++++++++++--- pubky/src/shared/pkarr.rs | 42 +++++++++++++---------- pubky/src/shared/public.rs | 70 +++++++++++++++++++++++++------------- pubky/src/wasm.rs | 16 +++------ 7 files changed, 110 insertions(+), 65 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index e3aa3f5..3852307 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -26,19 +26,22 @@ await client.signup(keypair, homeserver) const publicKey = keypair.public_key(); +// Pubky URL +let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + // Verify that you are signed in. const session = await client.session(publicKey) const body = Buffer.from(JSON.stringify({ foo: 'bar' })) // PUT public data, by authorized client -await client.put(publicKey, "/pub/example.com/arbitrary", body); +await client.put(url, body); // GET public data without signup or signin { const client = new PubkyClient(); - let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + let response = await client.get(url); } ``` diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 624f7dc..7fb4111 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -12,17 +12,19 @@ test('public: put/get', async (t) => { const publicKey = keypair.public_key(); + let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) // PUT public data, by authorized client - await client.put(publicKey, "/pub/example.com/arbitrary", body); + await client.put(url, body); // GET public data without signup or signin { const client = PubkyClient.testnet(); - let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + let response = await client.get(url); t.ok(Buffer.from(response).equals(body)) } diff --git a/pubky/src/native.rs b/pubky/src/native.rs index a2afea0..95f4a7d 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -86,13 +86,13 @@ impl PubkyClient { // === Public data === /// Upload a small payload to a given path. - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - self.inner_put(pubky, path, content).await + pub async fn put>(&self, url: T, content: &[u8]) -> Result<()> { + self.inner_put(url, content).await } /// Download a small payload from a given path relative to a pubky author. - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result> { - self.inner_get(pubky, path).await + pub async fn get>(&self, url: T) -> Result> { + self.inner_get(url).await } // /// Delete a file at a path relative to a pubky author. diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 0985d34..3f900ce 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -2,12 +2,15 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; +use url::Url; use crate::{ error::{Error, Result}, PubkyClient, }; +use super::pkarr::Endpoint; + impl PubkyClient { /// Signup to a homeserver and update Pkarr accordingly. /// @@ -22,7 +25,10 @@ impl PubkyClient { let public_key = &keypair.public_key(); - let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; + let Endpoint { + public_key: audience, + mut url, + } = self.resolve_endpoint(&homeserver).await?; url.set_path(&format!("/{}", public_key)); @@ -30,7 +36,11 @@ impl PubkyClient { .as_bytes() .to_owned(); - let response = self.request(Method::PUT, url).body(body).send().await?; + let response = self + .request(Method::PUT, url.clone()) + .body(body) + .send() + .await?; self.store_session(response); @@ -44,7 +54,7 @@ impl PubkyClient { /// Returns None if not signed in, or [reqwest::Error] /// if the response has any other `>=404` status code. pub(crate) async fn inner_session(&self, pubky: &PublicKey) -> Result> { - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); @@ -65,7 +75,10 @@ impl PubkyClient { /// Signout from a homeserver. pub async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + let Endpoint { + public_key, + mut url, + } = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); @@ -80,7 +93,10 @@ impl PubkyClient { pub async fn inner_signin(&self, keypair: &Keypair) -> Result<()> { let pubky = keypair.public_key(); - let (audience, mut url) = self.resolve_pubky_homeserver(&pubky).await?; + let Endpoint { + public_key: audience, + mut url, + } = self.resolve_pubky_homeserver(&pubky).await?; url.set_path(&format!("/{}/session", &pubky)); diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index 92f7bc8..f615de0 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -1,4 +1,4 @@ -use url::Url; +use url::{Origin, Url}; use pkarr::{ dns::{rdata::SVCB, Packet}, @@ -48,11 +48,8 @@ impl PubkyClient { } /// Resolve the homeserver for a pubky. - pub(crate) async fn resolve_pubky_homeserver( - &self, - pubky: &PublicKey, - ) -> Result<(PublicKey, Url)> { - let target = format!("_pubky.{}", pubky); + pub(crate) async fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result { + let target = format!("_pubky.{pubky}"); self.resolve_endpoint(&target) .await @@ -60,14 +57,14 @@ impl PubkyClient { } /// Resolve a service's public_key and clearnet url from a Pubky domain - pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result { let original_target = target; // TODO: cache the result of this function? let mut target = target.to_string(); let mut homeserver_public_key = None; - let mut host = target.clone(); + let mut origin = target.clone(); let mut step = 0; @@ -118,9 +115,9 @@ impl PubkyClient { } let port = u16::from_be_bytes([port[0], port[1]]); - host = format!("{target}:{port}"); + origin = format!("{target}:{port}"); } else { - host.clone_from(&target); + origin.clone_from(&target); }; if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION { @@ -130,20 +127,29 @@ impl PubkyClient { } } - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; + if let Some(public_key) = homeserver_public_key { + let mut url = Url::parse(&format!( + "{}://{}", + if origin.starts_with("localhost") { + "http" + } else { + "https" + }, + origin + ))?; - return Ok((homeserver, Url::parse(&url)?)); + return Ok(Endpoint { public_key, url }); } Err(Error::ResolveEndpoint(original_target.into())) } } +pub(crate) struct Endpoint { + pub public_key: PublicKey, + pub url: Url, +} + #[cfg(test)] mod tests { use super::*; @@ -200,7 +206,7 @@ mod tests { .await .unwrap(); - let (public_key, url) = client + let Endpoint { public_key, url } = client .resolve_pubky_homeserver(&pubky.public_key()) .await .unwrap(); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index b78554a..d3a776c 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -4,11 +4,16 @@ use pkarr::PublicKey; use reqwest::{Method, Response, StatusCode}; use url::Url; -use crate::{error::Result, PubkyClient}; +use crate::{ + error::{Error, Result}, + PubkyClient, +}; + +use super::pkarr::Endpoint; impl PubkyClient { - pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let url = self.url(pubky, path).await?; + pub async fn inner_put>(&self, url: T, content: &[u8]) -> Result<()> { + let url = self.pubky_to_http(url).await?; let response = self .request(Method::PUT, url) @@ -21,8 +26,8 @@ impl PubkyClient { Ok(()) } - pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result> { - let url = self.url(pubky, path).await?; + pub async fn inner_get>(&self, url: T) -> Result> { + let url = self.pubky_to_http(url).await?; let response = self.request(Method::GET, url).send().await?; @@ -38,8 +43,8 @@ impl PubkyClient { Ok(Some(bytes)) } - pub async fn inner_delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { - let url = self.url(pubky, path).await?; + pub async fn inner_delete>(&self, url: T) -> Result<()> { + let url = self.pubky_to_http(url).await?; let response = self.request(Method::DELETE, url).send().await?; @@ -48,12 +53,35 @@ impl PubkyClient { Ok(()) } - async fn url(&self, pubky: &PublicKey, path: &str) -> Result { - let path = normalize_path(path)?; + async fn pubky_to_http>(&self, url: T) -> Result { + let mut original_url: Url = url + .try_into() + .map_err(|e| Error::Generic("Invalid Url".to_string()))?; - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + if original_url.scheme() != "pubky" { + return Ok(original_url); + } - url.set_path(&format!("/{pubky}/{path}")); + let pubky = original_url + .host_str() + .ok_or(Error::Generic("Missing Pubky Url host".to_string()))? + .to_string(); + + let Endpoint { mut url, .. } = self + .resolve_pubky_homeserver(&PublicKey::try_from(pubky.clone())?) + .await?; + + let path = original_url.path_segments(); + + // TODO: replace if we move to subdomains instead of paths. + let mut split = url.path_segments_mut().unwrap(); + split.push(&pubky); + if let Some(segments) = path { + for segment in segments { + split.push(segment); + } + } + drop(split); Ok(url) } @@ -96,16 +124,11 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); - client - .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await - .unwrap(); + let url = format!("pubky://{}/pub/foo.txt", keypair.public_key()); - let response = client - .get(&keypair.public_key(), "/pub/foo.txt") - .await - .unwrap() - .unwrap(); + client.put(url.as_str(), &[0, 1, 2, 3, 4]).await.unwrap(); + + let response = client.get(url.as_str()).await.unwrap().unwrap(); assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); @@ -135,18 +158,19 @@ mod tests { let public_key = keypair.public_key(); + let url = format!("pubky://{public_key}/pub/foo.txt"); + let other_client = PubkyClient::test(&testnet); { let other = Keypair::random(); + // TODO: remove extra client after switching to subdomains. other_client .signup(&other, &server.public_key()) .await .unwrap(); - let response = other_client - .put(&public_key, "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await; + let response = other_client.put(url.as_str(), &[0, 1, 2, 3, 4]).await; match response { Err(Error::Reqwest(error)) => { diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index d22a6ed..782a5b0 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -5,7 +5,7 @@ use std::{ use wasm_bindgen::prelude::*; -use reqwest::{Method, RequestBuilder, Response}; +use reqwest::{IntoUrl, Method, RequestBuilder, Response}; use url::Url; use crate::PubkyClient; @@ -116,20 +116,14 @@ impl PubkyClient { #[wasm_bindgen] /// Upload a small payload to a given path. - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<(), JsValue> { - self.inner_put(pubky.as_inner(), path, content) - .await - .map_err(|e| e.into()) + pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> { + self.inner_put(url, content).await.map_err(|e| e.into()) } #[wasm_bindgen] /// Download a small payload from a given path relative to a pubky author. - pub async fn get( - &self, - pubky: &PublicKey, - path: &str, - ) -> Result, JsValue> { - self.inner_get(pubky.as_inner(), path) + pub async fn get(&self, url: &str) -> Result, JsValue> { + self.inner_get(url) .await .map(|b| b.map(|b| (&*b).into())) .map_err(|e| e.into()) From 7deac44fe36f7b1c19203405f8988d04a3c04b7a Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 31 Jul 2024 17:52:52 +0300 Subject: [PATCH 041/125] chore: publish 0.1.2 --- pubky/pkg/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index ad5c858..0543b28 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "repository": { "type": "git", From dcb178cfbbe5a8eeb950e4f5a46865ef2042d13b Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 1 Aug 2024 08:43:12 +0300 Subject: [PATCH 042/125] test(pubky): add tests for 401 and 403 responses --- pubky/pkg/test/public.js | 76 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 7fb4111..dd116b2 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -43,6 +43,78 @@ test('public: put/get', async (t) => { // } }) -test.skip("not found") +test("not found", async (t) => { + const client = PubkyClient.testnet(); -test.skip("unauthorized") + + const keypair = Keypair.random(); + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); + await client.signup(keypair, homeserver); + + const publicKey = keypair.public_key(); + + let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + + let result = await client.get(url).catch(e => e); + + t.ok(result instanceof Error); + t.is( + result.message, + `HTTP status client error (404 Not Found) for url (http://localhost:15411/${publicKey.z32()}/pub/example.com/arbitrary)` + ) +}) + +test("unauthorized", async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.public_key() + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + await client.signup(keypair, homeserver) + + const session = await client.session(publicKey) + t.ok(session, "signup") + + await client.signout(publicKey) + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + + // PUT public data, by authorized client + let result = await client.put(url, body).catch(e => e); + + t.ok(result instanceof Error); + t.is( + result.message, + `HTTP status client error (401 Unauthorized) for url (http://localhost:15411/${publicKey.z32()}/pub/example.com/arbitrary)` + ) +}) + +test("forbidden", async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.public_key() + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + await client.signup(keypair, homeserver) + + const session = await client.session(publicKey) + t.ok(session, "signup") + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + let url = `pubky://${publicKey.z32()}/priv/example.com/arbitrary`; + + // PUT public data, by authorized client + let result = await client.put(url, body).catch(e => e); + + t.ok(result instanceof Error); + t.is( + result.message, + `HTTP status client error (403 Forbidden) for url (http://localhost:15411/${publicKey.z32()}/priv/example.com/arbitrary)` + ) +}) From f760175541d779232eaff6c3732ba57c1bb471fb Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 1 Aug 2024 09:37:47 +0300 Subject: [PATCH 043/125] feat(pubky): add delete files by url --- pubky-homeserver/src/database/tables/blobs.rs | 17 +++-- .../src/database/tables/entries.rs | 25 +++++++ pubky-homeserver/src/routes.rs | 1 + pubky-homeserver/src/routes/public.rs | 73 +++++++++++++----- pubky/pkg/README.md | 3 + pubky/pkg/test/public.js | 33 ++++----- pubky/src/native.rs | 8 +- pubky/src/shared/public.rs | 74 ++++++++++--------- pubky/src/wasm.rs | 6 ++ 9 files changed, 160 insertions(+), 80 deletions(-) diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs index 80c46ae..3280f47 100644 --- a/pubky-homeserver/src/database/tables/blobs.rs +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -21,20 +21,25 @@ impl DB { public_key: &PublicKey, path: &str, ) -> anyhow::Result> { - let mut rtxn = self.env.read_txn()?; + let rtxn = self.env.read_txn()?; let mut key = vec![]; key.extend_from_slice(public_key.as_bytes()); key.extend_from_slice(path.as_bytes()); - if let Some(bytes) = self.tables.entries.get(&rtxn, &key)? { + let result = if let Some(bytes) = self.tables.entries.get(&rtxn, &key)? { let entry = Entry::deserialize(bytes)?; - if let Some(blob) = self.tables.blobs.get(&rtxn, entry.content_hash())? { - return Ok(Some(bytes::Bytes::from(blob.to_vec()))); - }; + self.tables + .blobs + .get(&rtxn, entry.content_hash())? + .map(|blob| bytes::Bytes::from(blob.to_vec())) + } else { + None }; - Ok(None) + rtxn.commit(); + + Ok(result) } } diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index a0e461b..ecadf18 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -58,6 +58,31 @@ impl DB { Ok(()) } + + pub fn delete_entry(&mut self, public_key: &PublicKey, path: &str) -> anyhow::Result { + let mut wtxn = self.env.write_txn()?; + + let mut key = vec![]; + key.extend_from_slice(public_key.as_bytes()); + key.extend_from_slice(path.as_bytes()); + + let deleted = if let Some(bytes) = self.tables.entries.get(&wtxn, &key)? { + let entry = Entry::deserialize(bytes)?; + + // TODO: reference counting of blobs + let deleted_blobs = self.tables.blobs.delete(&mut wtxn, entry.content_hash())?; + + let deleted_entry = self.tables.entries.delete(&mut wtxn, &key)?; + + deleted_entry & deleted_blobs + } else { + false + }; + + wtxn.commit()?; + + Ok(deleted) + } } #[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 0d3b7e3..4d057a3 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -30,6 +30,7 @@ fn base(state: AppState) -> Router { .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*path", put(public::put)) .route("/:pubky/*path", get(public::get)) + .route("/:pubky/*path", delete(public::delete)) .layer(CookieManagerLayer::new()) // TODO: revisit if we enable streaming big payloads // TODO: maybe add to a separate router (drive router?). diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 4a7834c..7cc48fe 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -7,6 +7,7 @@ use axum::{ }; use axum_extra::body::AsyncReadBody; use futures_util::stream::StreamExt; +use pkarr::PublicKey; use tower_cookies::Cookies; use tracing::debug; @@ -31,28 +32,17 @@ pub async fn put( mut body: Body, ) -> Result { let public_key = pubky.public_key().clone(); - let path = path.as_str().to_string(); + let path = path.as_str(); - // TODO: can we move this logic to the extractor or a layer - // to perform this validation? - let session = state - .db - .get_session(cookies, &public_key, &path)? - .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; - - if !path.starts_with("pub/") { - return Err(Error::new( - StatusCode::FORBIDDEN, - "Writing to directories other than '/pub/' is forbidden".into(), - )); - } - - // TODO: should we forbid paths ending with `/`? + authorize(&mut state, cookies, &public_key, path)?; + verify(path)?; let mut stream = body.into_data_stream(); let (tx, rx) = flume::bounded::(1); + let path = path.to_string(); + // TODO: refactor Database to clean up this scope. let done = tokio::task::spawn_blocking(move || -> Result<()> { // TODO: this is a blocking operation, which is ok for small @@ -84,7 +74,7 @@ pub async fn get( pubky: Pubky, path: EntryPath, ) -> Result { - // TODO: check the path, return an error if doesn't start with `/pub/` + verify(path.as_str()); // TODO: Enable streaming @@ -96,3 +86,52 @@ pub async fn get( Ok(None) => Err(Error::with_status(StatusCode::NOT_FOUND)), } } + +pub async fn delete( + State(mut state): State, + pubky: Pubky, + path: EntryPath, + cookies: Cookies, + mut body: Body, +) -> Result { + let public_key = pubky.public_key().clone(); + let path = path.as_str(); + + authorize(&mut state, cookies, &public_key, path)?; + verify(path)?; + + state.db.delete_entry(&public_key, path)?; + + // TODO: return relevant headers, like Etag? + + Ok(()) +} + +fn authorize( + state: &mut AppState, + cookies: Cookies, + public_key: &PublicKey, + path: &str, +) -> Result<()> { + // TODO: can we move this logic to the extractor or a layer + // to perform this validation? + let session = state + .db + .get_session(cookies, public_key, path)? + .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; + + Ok(()) +} + +fn verify(path: &str) -> Result<()> { + if !path.starts_with("pub/") { + return Err(Error::new( + StatusCode::FORBIDDEN, + "Writing to directories other than '/pub/' is forbidden".into(), + )); + } + + // TODO: should we forbid paths ending with `/`? + + Ok(()) +} diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 3852307..9f5fcb6 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -43,6 +43,9 @@ await client.put(url, body); let response = await client.get(url); } + +// Delete public data, by authorized client +await client.delete(url); ``` ## Test and Development diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index dd116b2..9a8b5c2 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -19,28 +19,25 @@ test('public: put/get', async (t) => { // PUT public data, by authorized client await client.put(url, body); + const otherClient = PubkyClient.testnet(); // GET public data without signup or signin { - const client = PubkyClient.testnet(); - - let response = await client.get(url); + let response = await otherClient.get(url); t.ok(Buffer.from(response).equals(body)) } - // // DELETE public data, by authorized client - // await client.delete(publicKey, "/pub/example.com/arbitrary"); - // - // - // // GET public data without signup or signin - // { - // const client = new PubkyClient(); - // - // let response = await client.get(publicKey, "/pub/example.com/arbitrary"); - // - // t.notOk(response) - // } + // DELETE public data, by authorized client + await client.delete(url); + + + // GET public data without signup or signin + { + let response = await otherClient.get(url); + + t.notOk(response) + } }) test("not found", async (t) => { @@ -58,11 +55,7 @@ test("not found", async (t) => { let result = await client.get(url).catch(e => e); - t.ok(result instanceof Error); - t.is( - result.message, - `HTTP status client error (404 Not Found) for url (http://localhost:15411/${publicKey.z32()}/pub/example.com/arbitrary)` - ) + t.notOk(result); }) test("unauthorized", async (t) => { diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 95f4a7d..783bce6 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -95,10 +95,10 @@ impl PubkyClient { self.inner_get(url).await } - // /// Delete a file at a path relative to a pubky author. - // pub async fn delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { - // self.inner_delete(pubky, path).await - // } + /// Delete a file at a path relative to a pubky author. + pub async fn delete>(&self, url: T) -> Result<()> { + self.inner_delete(url).await + } } // === Internals === diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index d3a776c..e9eeb0d 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -31,12 +31,12 @@ impl PubkyClient { let response = self.request(Method::GET, url).send().await?; - response.error_for_status_ref()?; - if response.status() == StatusCode::NOT_FOUND { return Ok(None); } + response.error_for_status_ref()?; + // TODO: bail on too large files. let bytes = response.bytes().await?; @@ -125,28 +125,23 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); let url = format!("pubky://{}/pub/foo.txt", keypair.public_key()); + let url = url.as_str(); - client.put(url.as_str(), &[0, 1, 2, 3, 4]).await.unwrap(); + client.put(url, &[0, 1, 2, 3, 4]).await.unwrap(); - let response = client.get(url.as_str()).await.unwrap().unwrap(); + let response = client.get(url).await.unwrap().unwrap(); assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); - // client - // .delete(&keypair.public_key(), "/pub/foo.txt") - // .await - // .unwrap(); - // - // let response = client - // .get(&keypair.public_key(), "/pub/foo.txt") - // .await - // .unwrap(); - // - // assert_eq!(response, None); + client.delete(url).await.unwrap(); + + let response = client.get(url).await.unwrap(); + + assert_eq!(response, None); } #[tokio::test] - async fn forbidden_put_delete() { + async fn unauthorized_put_delete() { let testnet = Testnet::new(10); let server = Homeserver::start_test(&testnet).await.unwrap(); @@ -159,6 +154,7 @@ mod tests { let public_key = keypair.public_key(); let url = format!("pubky://{public_key}/pub/foo.txt"); + let url = url.as_str(); let other_client = PubkyClient::test(&testnet); { @@ -170,7 +166,7 @@ mod tests { .await .unwrap(); - let response = other_client.put(url.as_str(), &[0, 1, 2, 3, 4]).await; + let response = other_client.put(url, &[0, 1, 2, 3, 4]).await; match response { Err(Error::Reqwest(error)) => { @@ -182,21 +178,33 @@ mod tests { } } - // client - // .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) - // .await - // .unwrap(); - // - // client - // .delete(&keypair.public_key(), "/pub/foo.txt") - // .await - // .unwrap(); - // - // let response = client - // .get(&keypair.public_key(), "/pub/foo.txt") - // .await - // .unwrap(); - // - // assert_eq!(response, None); + client.put(url, &[0, 1, 2, 3, 4]).await.unwrap(); + + { + let other = Keypair::random(); + + // TODO: remove extra client after switching to subdomains. + other_client + .signup(&other, &server.public_key()) + .await + .unwrap(); + + let response = other_client.delete(url).await; + + dbg!(&response); + + match response { + Err(Error::Reqwest(error)) => { + assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) + } + error => { + panic!("expected error StatusCode::UNAUTHORIZED") + } + } + } + + let response = client.get(url).await.unwrap().unwrap(); + + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); } } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 782a5b0..43a5080 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -128,4 +128,10 @@ impl PubkyClient { .map(|b| b.map(|b| (&*b).into())) .map_err(|e| e.into()) } + + #[wasm_bindgen] + /// Delete a file at a path relative to a pubky author. + pub async fn delete(&self, url: &str) -> Result<(), JsValue> { + self.inner_delete(url).await.map_err(|e| e.into()) + } } From 86b7dfd4a4f3b18234a6fca6af8a7dd45a9ee84e Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 1 Aug 2024 09:38:51 +0300 Subject: [PATCH 044/125] chore: publish 0.1.3 --- pubky/pkg/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 0543b28..f940a5f 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "repository": { "type": "git", From ab4a8c309cb9073bb98cbbd6a1ae678f7530f7ce Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 1 Aug 2024 11:43:13 +0300 Subject: [PATCH 045/125] chore(js): publish 0.1.4 --- pubky/pkg/README.md | 8 +++----- pubky/pkg/package.json | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 9f5fcb6..d44efc2 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -71,12 +71,10 @@ Run the local testnet server npm run testnet ``` -Pass the logged addresses as inputs to `PubkyClient` +Use the logged addresses as inputs to `PubkyClient` ```js -import { PubkyClient, PublicKey } from '../index.js' +import { PubkyClient } from '../index.js' -const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]); - -let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); +const client = new PubkyClient().testnet(); ``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index f940a5f..53b52b5 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "repository": { "type": "git", @@ -14,7 +14,6 @@ "test-nodejs": "tape test/*.js -cov", "test-browser": "browserify test/*.js -p esmify | npx tape-run", "build": "cargo run --bin bundle_pubky_npm", - "preinstall": "npm run build", "prepublishOnly": "npm run build && npm run test" }, "files": [ From 381a634692194bd42796eec3eabc672545bec62a Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 1 Aug 2024 16:04:25 +0300 Subject: [PATCH 046/125] chore(js): publish 0.1.7 --- pubky/pkg/.gitignore | 2 +- pubky/pkg/README.md | 2 +- pubky/pkg/package.json | 15 +++++++-------- pubky/pkg/test/auth.js | 2 +- pubky/pkg/test/keys.js | 2 +- pubky/pkg/test/public.js | 8 ++++---- pubky/src/wasm/keys.rs | 2 +- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore index bc0022f..d2a005f 100644 --- a/pubky/pkg/.gitignore +++ b/pubky/pkg/.gitignore @@ -1,5 +1,5 @@ -nodejs/* browser.js coverage node_modules package-lock.json +pubky* diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index d44efc2..fbe89d2 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -24,7 +24,7 @@ let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn4 await client.signup(keypair, homeserver) -const publicKey = keypair.public_key(); +const publicKey = keypair.publicKey(); // Pubky URL let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 53b52b5..73ee93c 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.4", + "version": "0.1.7", "license": "MIT", "repository": { "type": "git", @@ -17,16 +17,15 @@ "prepublishOnly": "npm run build && npm run test" }, "files": [ - "nodejs/*", - "index.js", - "browser.js" + "nodejs/pubky_bg.wasm", + "nodejs/pubky.js", + "nodejs/pubky.d.ts", + "browser.js", + "index.js" ], "main": "index.js", "browser": "browser.js", - "types": "pubky.d.ts", - "sideEffects": [ - "./snippets/*" - ], + "types": "nodejs/pubky.d.ts", "keywords": [ "web", "dht", diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index d738c99..7b15483 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -6,7 +6,7 @@ test('auth', async (t) => { const client = PubkyClient.testnet(); const keypair = Keypair.random() - const publicKey = keypair.public_key() + const publicKey = keypair.publicKey() const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') await client.signup(keypair, homeserver) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index 306e21e..377160a 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -7,7 +7,7 @@ test('generate keys from a seed', async (t) => { const keypair = Keypair.from_secret_key(secretkey) - const publicKey = keypair.public_key() + const publicKey = keypair.publicKey() t.is(publicKey.z32(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') }) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 9a8b5c2..cd1cb5c 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -10,7 +10,7 @@ test('public: put/get', async (t) => { const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); await client.signup(keypair, homeserver); - const publicKey = keypair.public_key(); + const publicKey = keypair.publicKey(); let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; @@ -49,7 +49,7 @@ test("not found", async (t) => { const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); await client.signup(keypair, homeserver); - const publicKey = keypair.public_key(); + const publicKey = keypair.publicKey(); let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; @@ -62,7 +62,7 @@ test("unauthorized", async (t) => { const client = PubkyClient.testnet(); const keypair = Keypair.random() - const publicKey = keypair.public_key() + const publicKey = keypair.publicKey() const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') await client.signup(keypair, homeserver) @@ -90,7 +90,7 @@ test("forbidden", async (t) => { const client = PubkyClient.testnet(); const keypair = Keypair.random() - const publicKey = keypair.public_key() + const publicKey = keypair.publicKey() const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') await client.signup(keypair, homeserver) diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index d1ef078..ea5ea2c 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -22,7 +22,7 @@ impl Keypair { Self(pkarr::Keypair::from_secret_key(&bytes)) } - #[wasm_bindgen] + #[wasm_bindgen(js_name = "publicKey")] /// Returns the [PublicKey] of this keypair. pub fn public_key(&self) -> PublicKey { PublicKey(self.0.public_key()) From d39958915f9b5a20b3b77ab5312549e76b778c2a Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 1 Aug 2024 16:48:18 +0300 Subject: [PATCH 047/125] chore(js): publish 0.1.9 --- pubky/pkg/.gitignore | 1 + pubky/pkg/index.js | 1 - pubky/pkg/package.json | 13 ++++++------- pubky/pkg/test/auth.js | 2 +- pubky/pkg/test/keys.js | 2 +- pubky/pkg/test/public.js | 2 +- pubky/src/bin/patch.mjs | 11 ++++++++++- 7 files changed, 20 insertions(+), 12 deletions(-) delete mode 100644 pubky/pkg/index.js diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore index d2a005f..7355b75 100644 --- a/pubky/pkg/.gitignore +++ b/pubky/pkg/.gitignore @@ -1,3 +1,4 @@ +index.cjs browser.js coverage node_modules diff --git a/pubky/pkg/index.js b/pubky/pkg/index.js deleted file mode 100644 index 0af4fe0..0000000 --- a/pubky/pkg/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './nodejs/pubky.js' diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 73ee93c..182864d 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.7", + "version": "0.1.9", "license": "MIT", "repository": { "type": "git", @@ -17,15 +17,14 @@ "prepublishOnly": "npm run build && npm run test" }, "files": [ - "nodejs/pubky_bg.wasm", - "nodejs/pubky.js", - "nodejs/pubky.d.ts", + "index.cjs", "browser.js", - "index.js" + "pubky.d.ts", + "pubky_bg.wasm" ], - "main": "index.js", + "main": "index.cjs", "browser": "browser.js", - "types": "nodejs/pubky.d.ts", + "types": "pubky.d.ts", "keywords": [ "web", "dht", diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 7b15483..d193dfe 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -1,6 +1,6 @@ import test from 'tape' -import { PubkyClient, Keypair, PublicKey } from '../index.js' +import { PubkyClient, Keypair, PublicKey } from '../index.cjs' test('auth', async (t) => { const client = PubkyClient.testnet(); diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index 377160a..e7243f1 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -1,6 +1,6 @@ import test from 'tape' -import { Keypair } from '../index.js' +import { Keypair } from '../index.cjs' test('generate keys from a seed', async (t) => { const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex') diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index cd1cb5c..2e6c988 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -1,6 +1,6 @@ import test from 'tape' -import { PubkyClient, Keypair, PublicKey } from '../index.js' +import { PubkyClient, Keypair, PublicKey } from '../index.cjs' test('public: put/get', async (t) => { const client = PubkyClient.testnet(); diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs index ebbf13b..0e9ebe9 100644 --- a/pubky/src/bin/patch.mjs +++ b/pubky/src/bin/patch.mjs @@ -2,7 +2,7 @@ // // Based on hacks from [this issue](https://github.com/rustwasm/wasm-pack/issues/1334) -import { readFile, writeFile } from "node:fs/promises"; +import { readFile, writeFile, rename } from "node:fs/promises"; import { fileURLToPath } from 'node:url'; import path, { dirname } from 'node:path'; @@ -57,3 +57,12 @@ const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `. ); await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched); + +// Move outside of nodejs + +await Promise.all([".js", ".d.ts", "_bg.wasm"].map(suffix => + rename( + path.join(__dirname, `../../pkg/nodejs/${name}${suffix}`), + path.join(__dirname, `../../pkg/${suffix === '.js' ? "index.cjs" : (name + suffix)}`), + )) +) From fb3823c66149d0663dd4ed733820c72e105c01e6 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 3 Aug 2024 13:45:14 +0300 Subject: [PATCH 048/125] feat(js): add Keypair.secretKey --- pubky/src/wasm/keys.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index ea5ea2c..fd82c4c 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -13,8 +13,8 @@ impl Keypair { Self(pkarr::Keypair::random()) } - #[wasm_bindgen] /// Generate a [Keypair] from a secret key. + #[wasm_bindgen(js_name = "fromSecretKey")] pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { let mut bytes = [0; 32]; secret_key.copy_to(&mut bytes); @@ -22,8 +22,14 @@ impl Keypair { Self(pkarr::Keypair::from_secret_key(&bytes)) } - #[wasm_bindgen(js_name = "publicKey")] + /// Returns the secret key of this keypair. + #[wasm_bindgen(js_name = "secretKey")] + pub fn secret_key(&self) -> js_sys::Uint8Array { + self.0.secret_key().as_slice().into() + } + /// Returns the [PublicKey] of this keypair. + #[wasm_bindgen(js_name = "publicKey")] pub fn public_key(&self) -> PublicKey { PublicKey(self.0.public_key()) } From 5ee464a5b4ef12aa24ed6dfa1dccb01b999c848c Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 3 Aug 2024 15:46:20 +0300 Subject: [PATCH 049/125] feat(pubky-common): add encrypt/decrypt with secret_box --- Cargo.lock | 118 +++++++++++++++++++++++++++++++++++++ pubky-common/Cargo.toml | 1 + pubky-common/src/crypto.rs | 43 ++++++++++++++ 3 files changed, 162 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 85d39a1..040fe82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -81,6 +91,18 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -269,6 +291,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "blake3" version = "1.5.2" @@ -321,6 +352,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.5.11" @@ -465,9 +507,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -522,6 +580,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -730,6 +789,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -960,6 +1020,15 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1165,6 +1234,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "overload" version = "0.1.1" @@ -1204,6 +1279,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1320,6 +1406,17 @@ dependencies = [ "spki", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "postcard" version = "1.0.8" @@ -1363,6 +1460,7 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" name = "pubky" version = "0.1.0" dependencies = [ + "argon2", "bytes", "js-sys", "pkarr", @@ -1382,6 +1480,7 @@ version = "0.1.0" dependencies = [ "base32", "blake3", + "crypto_secretbox", "ed25519-dalek", "js-sys", "once_cell", @@ -1594,6 +1693,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2124,6 +2232,16 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.5.2" diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 6855f97..042474c 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -15,6 +15,7 @@ rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } serde = { version = "1.0.204", features = ["derive"] } +crypto_secretbox = "0.1.1" [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.69" diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index ec8f58a..5e57f51 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -1,3 +1,7 @@ +use crypto_secretbox::{ + aead::{Aead, AeadCore, KeyInit, OsRng}, + XSalsa20Poly1305, +}; use rand::prelude::Rng; pub use pkarr::{Keypair, PublicKey}; @@ -25,3 +29,42 @@ pub fn random_bytes() -> [u8; N] { } arr } + +pub fn encrypt( + plain_text: &[u8], + encryption_key: &[u8; 32], +) -> Result, crypto_secretbox::Error> { + let cipher = XSalsa20Poly1305::new(encryption_key.into()); + let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng); // unique per message + let ciphertext = cipher.encrypt(&nonce, plain_text)?; + + let mut out: Vec = Vec::with_capacity(nonce.len() + ciphertext.len()); + out.extend_from_slice(nonce.as_slice()); + out.extend_from_slice(&ciphertext); + + Ok(out) +} + +pub fn decrypt( + bytes: &[u8], + encryption_key: &[u8; 32], +) -> Result, crypto_secretbox::Error> { + let cipher = XSalsa20Poly1305::new(encryption_key.into()); + cipher.decrypt(bytes[..24].into(), &bytes[24..]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_decrypt() { + let plain_text = "Plain text!"; + let encryption_key = [0; 32]; + + let encrypted = encrypt(plain_text.as_bytes(), &encryption_key).unwrap(); + let decrypted = decrypt(&encrypted, &encryption_key).unwrap(); + + assert_eq!(decrypted, plain_text.as_bytes()) + } +} From 11334bf81c47520ffdfb43c384fcdfd9799f858c Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 3 Aug 2024 16:33:38 +0300 Subject: [PATCH 050/125] feat(pubky): add recovery file generation and decryption --- pubky-common/Cargo.toml | 2 +- pubky-common/src/crypto.rs | 19 ++++---- pubky/Cargo.toml | 1 + pubky/src/error.rs | 23 ++++++++- pubky/src/native.rs | 15 +++++- pubky/src/shared/mod.rs | 1 + pubky/src/shared/recovery_file.rs | 81 +++++++++++++++++++++++++++++++ 7 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 pubky/src/shared/recovery_file.rs diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 042474c..0a9df3b 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -15,7 +15,7 @@ rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } serde = { version = "1.0.204", features = ["derive"] } -crypto_secretbox = "0.1.1" +crypto_secretbox = { version = "0.1.1", features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.69" diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index 5e57f51..a7adea5 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -30,10 +30,7 @@ pub fn random_bytes() -> [u8; N] { arr } -pub fn encrypt( - plain_text: &[u8], - encryption_key: &[u8; 32], -) -> Result, crypto_secretbox::Error> { +pub fn encrypt(plain_text: &[u8], encryption_key: &[u8; 32]) -> Result, Error> { let cipher = XSalsa20Poly1305::new(encryption_key.into()); let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng); // unique per message let ciphertext = cipher.encrypt(&nonce, plain_text)?; @@ -45,12 +42,16 @@ pub fn encrypt( Ok(out) } -pub fn decrypt( - bytes: &[u8], - encryption_key: &[u8; 32], -) -> Result, crypto_secretbox::Error> { +pub fn decrypt(bytes: &[u8], encryption_key: &[u8; 32]) -> Result, Error> { let cipher = XSalsa20Poly1305::new(encryption_key.into()); - cipher.decrypt(bytes[..24].into(), &bytes[24..]) + + Ok(cipher.decrypt(bytes[..24].into(), &bytes[24..])?) +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + SecretBox(#[from] crypto_secretbox::Error), } #[cfg(test)] diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 392402e..149b6be 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -17,6 +17,7 @@ url = "2.5.2" bytes = "1.6.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } +argon2 = { version = "0.5.3", features = ["std"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pkarr = { version="2.1.0", features = ["async"] } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 501168d..e27814b 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -12,6 +12,22 @@ pub enum Error { #[error("Generic error: {0}")] Generic(String), + #[error("Could not resolve endpoint for {0}")] + ResolveEndpoint(String), + + // === Recovery file == + #[error("Recovery file should start with a spec line, followed by a new line character")] + RecoveryFileMissingSpecLine, + + #[error("Recovery file should start with a spec line, followed by a new line character")] + RecoveryFileVersionNotSupported, + + #[error("Recovery file should contain an encrypted secret key after the new line character")] + RecoverFileMissingEncryptedSecretKey, + + #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")] + RecoverFileInvalidSecretKeyLength(usize), + // === Transparent === #[error(transparent)] Dns(#[from] SimpleDnsError), @@ -28,8 +44,11 @@ pub enum Error { #[error(transparent)] Session(#[from] pubky_common::session::Error), - #[error("Could not resolve endpoint for {0}")] - ResolveEndpoint(String), + #[error(transparent)] + Crypto(#[from] pubky_common::crypto::Error), + + #[error(transparent)] + Argon(#[from] argon2::Error), } #[cfg(target_arch = "wasm32")] diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 783bce6..620c94f 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -10,7 +10,11 @@ use pubky_common::session::Session; use reqwest::{Method, RequestBuilder, Response}; use url::Url; -use crate::{error::Result, PubkyClient}; +use crate::{ + error::Result, + shared::recovery_file::{create_recovery_file, decrypt_recovery_file}, + PubkyClient, +}; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -99,6 +103,15 @@ impl PubkyClient { pub async fn delete>(&self, url: T) -> Result<()> { self.inner_delete(url).await } + + // === Helpers === + + pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { + create_recovery_file(keypair, passphrase) + } + pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { + decrypt_recovery_file(recovery_file, passphrase) + } } // === Internals === diff --git a/pubky/src/shared/mod.rs b/pubky/src/shared/mod.rs index ec9bd27..49f11bd 100644 --- a/pubky/src/shared/mod.rs +++ b/pubky/src/shared/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod pkarr; pub mod public; +pub mod recovery_file; diff --git a/pubky/src/shared/recovery_file.rs b/pubky/src/shared/recovery_file.rs new file mode 100644 index 0000000..5f500ff --- /dev/null +++ b/pubky/src/shared/recovery_file.rs @@ -0,0 +1,81 @@ +use argon2::Argon2; +use pkarr::Keypair; +use pubky_common::crypto::{decrypt, encrypt}; + +use crate::{ + error::{Error, Result}, + PubkyClient, +}; + +static SPEC_NAME: &str = "recovery"; +static SPEC_LINE: &str = "pubky.org/recovery"; + +pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { + let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; + + let mut split = recovery_file.split(|byte| byte == &10); + + match split.next() { + Some(bytes) => { + if !(bytes.starts_with(SPEC_LINE.as_bytes()) + || bytes.starts_with(b"pkarr.org/recovery")) + { + return Err(Error::RecoveryFileVersionNotSupported); + } + } + None => return Err(Error::RecoveryFileMissingSpecLine), + }; + + if let Some(encrypted) = split.next() { + let decrypted = decrypt(encrypted, &encryption_key)?; + let length = decrypted.len(); + let secret_key: [u8; 32] = decrypted + .try_into() + .map_err(|_| Error::RecoverFileInvalidSecretKeyLength(length))?; + + return Ok(Keypair::from_secret_key(&secret_key)); + }; + + Err(Error::RecoverFileMissingEncryptedSecretKey) +} + +pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { + let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; + let secret_key = keypair.secret_key(); + + let encrypted_secret_key = encrypt(&secret_key, &encryption_key)?; + + let mut out = Vec::with_capacity(SPEC_LINE.len() + 1 + encrypted_secret_key.len()); + + out.extend_from_slice(SPEC_LINE.as_bytes()); + out.extend_from_slice(b"\n"); + out.extend_from_slice(&encrypted_secret_key); + + Ok(out) +} + +fn recovery_file_encryption_key_from_passphrase(passphrase: &str) -> Result<[u8; 32]> { + let argon2id = Argon2::default(); + + let mut out = [0; 32]; + + argon2id.hash_password_into(passphrase.as_bytes(), SPEC_NAME.as_bytes(), &mut out)?; + + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_decrypt_recovery_file() { + let passphrase = "very secure password"; + let keypair = Keypair::random(); + + let recovery_file = PubkyClient::create_recovery_file(&keypair, passphrase).unwrap(); + let recovered = PubkyClient::decrypt_recovery_file(&recovery_file, passphrase).unwrap(); + + assert_eq!(recovered.public_key(), keypair.public_key()); + } +} From b584703ab5d0fd6095d921b7d7553b4367eed584 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 4 Aug 2024 14:18:35 +0300 Subject: [PATCH 051/125] feat(js): rename from_secret_key to fromSecretKey --- pubky/pkg/test/keys.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index e7243f1..e184b3b 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -5,7 +5,7 @@ import { Keypair } from '../index.cjs' test('generate keys from a seed', async (t) => { const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex') - const keypair = Keypair.from_secret_key(secretkey) + const keypair = Keypair.fromSecretKey(secretkey) const publicKey = keypair.publicKey() From 3bf89f1e9ea9276c3d1df6612d9ee2dc94797764 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 4 Aug 2024 14:19:20 +0300 Subject: [PATCH 052/125] feat(js): add createRecoveryFile and decryptRecoveryFile --- pubky/pkg/test/recovery.js | 19 ++++++++++++++++ pubky/src/native.rs | 4 ++++ pubky/src/wasm.rs | 44 +++++++++++++++++++++++++++----------- pubky/src/wasm/keys.rs | 6 ++++++ 4 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 pubky/pkg/test/recovery.js diff --git a/pubky/pkg/test/recovery.js b/pubky/pkg/test/recovery.js new file mode 100644 index 0000000..cf05160 --- /dev/null +++ b/pubky/pkg/test/recovery.js @@ -0,0 +1,19 @@ +import test from 'tape' + +import { PubkyClient, Keypair } from '../index.cjs' + +test('recovery', async (t) => { + const keypair = Keypair.random(); + + const recoveryFile = PubkyClient.createRecoveryFile(keypair, 'very secure password'); + + t.is(recoveryFile.length, 91) + t.deepEqual( + Array.from(recoveryFile.slice(0, 19)), + [112, 117, 98, 107, 121, 46, 111, 114, 103, 47, 114, 101, 99, 111, 118, 101, 114, 121, 10] + ) + + const recovered = PubkyClient.decryptRecoveryFile(recoveryFile, 'very secure password') + + t.is(recovered.publicKey().z32(), keypair.publicKey().z32()) +}) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 620c94f..6fc3e72 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -106,9 +106,13 @@ impl PubkyClient { // === Helpers === + /// Create a recovery file of the `keypair`, containing the secret key encrypted + /// using the `passphrase`. pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { create_recovery_file(keypair, passphrase) } + + /// Recover a keypair from a recovery file by decrypting the secret key using `passphrase`. pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { decrypt_recovery_file(recovery_file, passphrase) } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 43a5080..7f44e44 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -8,7 +8,10 @@ use wasm_bindgen::prelude::*; use reqwest::{IntoUrl, Method, RequestBuilder, Response}; use url::Url; -use crate::PubkyClient; +use crate::{ + shared::recovery_file::{create_recovery_file, decrypt_recovery_file}, + PubkyClient, +}; mod http; mod keys; @@ -49,28 +52,43 @@ impl PubkyClient { } } + /// Create a recovery file of the `keypair`, containing the secret key encrypted + /// using the `passphrase`. + #[wasm_bindgen(js_name = "createRecoveryFile")] + pub fn create_recovery_file( + keypair: &Keypair, + passphrase: &str, + ) -> Result { + create_recovery_file(keypair.as_inner(), passphrase) + .map(|b| b.as_slice().into()) + .map_err(|e| e.into()) + } + + /// Create a recovery file of the `keypair`, containing the secret key encrypted + /// using the `passphrase`. + #[wasm_bindgen(js_name = "decryptRecoveryFile")] + pub fn decrypt_recovery_file( + recovery_file: &[u8], + passphrase: &str, + ) -> Result { + decrypt_recovery_file(recovery_file, passphrase) + .map(Keypair::from) + .map_err(|e| e.into()) + } + /// Set Pkarr relays used for publishing and resolving Pkarr packets. /// /// By default, [PubkyClient] will use `["https://relay.pkarr.org"]` #[wasm_bindgen(js_name = "setPkarrRelays")] - pub fn set_pkarr_relays(mut self, relays: Vec) -> Self { - let relays: Vec = relays - .into_iter() - .filter_map(|name| name.as_string()) - .collect(); - + pub fn set_pkarr_relays(mut self, relays: Vec) -> Self { self.pkarr_relays = relays; self } // Read the set of pkarr relays used by this client. #[wasm_bindgen(js_name = "getPkarrRelays")] - pub fn get_pkarr_relays(&self) -> Vec { - self.pkarr_relays - .clone() - .into_iter() - .map(JsValue::from) - .collect() + pub fn get_pkarr_relays(&self) -> Vec { + self.pkarr_relays.clone() } /// Signup to a homeserver and update Pkarr accordingly. diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index fd82c4c..345e721 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -41,6 +41,12 @@ impl Keypair { } } +impl From for Keypair { + fn from(keypair: pkarr::Keypair) -> Self { + Self(keypair) + } +} + #[wasm_bindgen] pub struct PublicKey(pkarr::PublicKey); From 7ec19f95f31760bdbe82a2297ba8a43764d1af1a Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 4 Aug 2024 14:19:55 +0300 Subject: [PATCH 053/125] docs(js): update README with API documentation --- pubky/pkg/README.md | 110 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index fbe89d2..c2f89d0 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -2,6 +2,12 @@ JavaScript implementation of [Pubky](https://github.com/pubky/pubky). +## Table of Contents +- [Install](#install) +- [Getting Started](#getting-started) +- [API](#api) +- [Test and Development](#test-and-development) + ## Install ```bash @@ -48,6 +54,110 @@ await client.put(url, body); await client.delete(url); ``` +## API + +### PubkyClient + +#### constructor +```js +let client = new PubkyClient() +``` + +#### createRecoveryFile +```js +let recoveryFile = PubkyClient.createRecoveryFile(keypair, passphrase) +``` +- keypair: An instance of [Keypair](#keypair). +- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). +- Returns: A recovery file with a spec line and an encrypted secret key. + +#### createRecoveryFile +```js +let keypair = PubkyClient.decryptRecoveryfile(recoveryFile, passphrase) +``` +- recoveryFile: An instance of Uint8Array containing the recovery file blob. +- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). +- Returns: An instance of [Keypair](#keypair). + +#### signup +```js +await client.signup(keypair, homeserver) +``` +- keypair: An instance of [Keypair](#keypair). +- homeserver: An instance of [PublicKey](#publickey) representing the homeserver. + +#### session +```js +let session = await client.session(publicKey) +``` +- publicKey: An instance of [PublicKey](#publickey). +- Returns: A session object if signed in, or undefined if not. + +#### put +```js +let response = await client.put(url, body); +``` +- url: A string representing the Pubky URL. +- body: A Buffer containing the data to be stored. + +### get +```js +let response = await client.get(url) +``` +- url: A string representing the Pubky URL. +- Returns: A response object containing the requested data. + +### delete + +```js +let response = await delete(url); +``` +- url: A string representing the Pubky URL. + +### Keypair + +#### random +```js +let keypair = Keypair.random() +``` +- Returns: A new random Keypair. + +#### fromSecretKey +```js +let keypair = Keypair.fromSecretKey(secretKey) +``` +- secretKey: A 32 bytes Uint8array. +- Returns: A new Keypair. + + +#### publicKey +```js +let publicKey = keypair.publicKey() +``` +- Returns: The [PublicKey](#publickey) associated with the Keypair. + +#### secretKey +```js +let secretKey = keypair.secretKey() +``` +- Returns: The Uint8array secret key associated with the Keypair. + +### PublicKey + +#### from + +```js +let publicKey = PublicKey.from(string); +``` +- string: A string representing the public key. +- Returns: A new PublicKey instance. + +#### z32 +```js +let pubky = publicKey.z32(); +``` +Returns: The z-base-32 encoded string representation of the PublicKey. + ## Test and Development For test and development, you can run a local homeserver in a test network. From 4f59db42b1d636bdbeeaea2bf9466a41f6ba3875 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 4 Aug 2024 14:21:10 +0300 Subject: [PATCH 054/125] chore(js): publish 0.1.10 --- pubky/pkg/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 182864d..c66153e 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.9", + "version": "0.1.10", "license": "MIT", "repository": { "type": "git", From 9ec6ec58fd8d946b725bcd9269125a886f9cdf43 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 4 Aug 2024 16:30:59 +0300 Subject: [PATCH 055/125] feat(pubky): simple list api with reverse option --- pubky-homeserver/src/database/tables/blobs.rs | 4 +- .../src/database/tables/entries.rs | 55 +++++++++++-- pubky-homeserver/src/routes/public.rs | 29 ++++++- pubky/src/shared/public.rs | 80 ++++++++++++++++++- 4 files changed, 151 insertions(+), 17 deletions(-) diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs index 3280f47..f65dbe9 100644 --- a/pubky-homeserver/src/database/tables/blobs.rs +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -23,9 +23,7 @@ impl DB { ) -> anyhow::Result> { let rtxn = self.env.read_txn()?; - let mut key = vec![]; - key.extend_from_slice(public_key.as_bytes()); - key.extend_from_slice(path.as_bytes()); + let key = format!("{public_key}/{path}"); let result = if let Some(bytes) = self.tables.entries.get(&rtxn, &key)? { let entry = Entry::deserialize(bytes)?; diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index ecadf18..75aefff 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -1,7 +1,7 @@ use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, time::SystemTime}; +use std::{borrow::Cow, fmt::Result, time::SystemTime}; use heed::{ types::{Bytes, Str}, @@ -16,10 +16,12 @@ use pubky_common::{ use crate::database::DB; /// full_path(pubky/*path) => Entry. -pub type EntriesTable = Database; +pub type EntriesTable = Database; pub const ENTRIES_TABLE: &str = "entries"; +const MAX_LIST_LIMIT: i32 = 100; + impl DB { pub fn put_entry( &mut self, @@ -48,9 +50,7 @@ impl DB { entry.set_content_hash(hash); entry.set_content_length(length); - let mut key = vec![]; - key.extend_from_slice(public_key.as_bytes()); - key.extend_from_slice(path.as_bytes()); + let key = format!("{public_key}/{path}"); self.tables.entries.put(&mut wtxn, &key, &entry.serialize()); @@ -62,9 +62,7 @@ impl DB { pub fn delete_entry(&mut self, public_key: &PublicKey, path: &str) -> anyhow::Result { let mut wtxn = self.env.write_txn()?; - let mut key = vec![]; - key.extend_from_slice(public_key.as_bytes()); - key.extend_from_slice(path.as_bytes()); + let key = format!("{public_key}/{path}"); let deleted = if let Some(bytes) = self.tables.entries.get(&wtxn, &key)? { let entry = Entry::deserialize(bytes)?; @@ -83,6 +81,47 @@ impl DB { Ok(deleted) } + + /// Return a list of pubky urls. + /// + /// - limit defaults to and capped by [MAX_LIST_LIMIT] + pub fn list( + &self, + public_key: &PublicKey, + prefix: &str, + reverse: bool, + limit: Option, + ) -> anyhow::Result> { + let db = self.tables.entries; + let txn = self.env.read_txn()?; + + let prefix = format!("{public_key}/{prefix}"); + let limit = limit.unwrap_or(MAX_LIST_LIMIT).max(MAX_LIST_LIMIT); + + // Vector to store results + let mut results = Vec::new(); + + // Fetch data based on direction + if reverse { + let mut iter = self.tables.entries.rev_prefix_iter(&txn, &prefix)?; + + for _ in 0..limit { + if let Some((key, _)) = iter.next().transpose()? { + results.push(format!("pubky://{}", key)) + }; + } + } else { + let mut iter = self.tables.entries.prefix_iter(&txn, &prefix)?; + + for _ in 0..limit { + if let Some((key, _)) = iter.next().transpose()? { + results.push(format!("pubky://{}", key)) + }; + } + } + + Ok(results) + } } #[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 7cc48fe..79a1637 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -1,6 +1,9 @@ +use std::collections::HashMap; + use axum::{ body::{Body, Bytes}, - extract::{Path, State}, + debug_handler, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, RequestExt, Router, @@ -8,6 +11,7 @@ use axum::{ use axum_extra::body::AsyncReadBody; use futures_util::stream::StreamExt; use pkarr::PublicKey; +use serde::Deserialize; use tower_cookies::Cookies; use tracing::debug; @@ -73,13 +77,25 @@ pub async fn get( State(mut state): State, pubky: Pubky, path: EntryPath, + Query(params): Query>, ) -> Result { verify(path.as_str()); + let public_key = pubky.public_key(); + + if params.contains_key("list") { + // Handle listing + let vec = state.db.list( + public_key, + path.as_str(), + params.contains_key("reverse"), + params.get("limit").and_then(|l| l.parse::().ok()), + )?; + + return Ok(vec.join("\n").into()); + } // TODO: Enable streaming - let public_key = pubky.public_key(); - match state.db.get_blob(public_key, path.as_str()) { Err(error) => Err(error)?, Ok(Some(bytes)) => Ok(bytes), @@ -100,7 +116,12 @@ pub async fn delete( authorize(&mut state, cookies, &public_key, path)?; verify(path)?; - state.db.delete_entry(&public_key, path)?; + let deleted = state.db.delete_entry(&public_key, path)?; + + if !deleted { + // TODO: if the path ends with `/` return a `CONFLICT` error? + return Err(Error::with_status(StatusCode::NOT_FOUND)); + } // TODO: return relevant headers, like Etag? diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index e9eeb0d..c66af64 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -53,6 +53,28 @@ impl PubkyClient { Ok(()) } + pub async fn list>(&self, url: T, reverse: bool) -> Result> { + let mut url = self.pubky_to_http(url).await?; + + if reverse { + url.set_query("list&reverse".into()); + } else { + url.set_query("list".into()); + } + + let response = self.request(Method::GET, url).send().await?; + + response.error_for_status_ref()?; + + // TODO: bail on too large files. + let bytes = response.bytes().await?; + + Ok(String::from_utf8_lossy(&bytes) + .lines() + .map(String::from) + .collect()) + } + async fn pubky_to_http>(&self, url: T) -> Result { let mut original_url: Url = url .try_into() @@ -191,8 +213,6 @@ mod tests { let response = other_client.delete(url).await; - dbg!(&response); - match response { Err(Error::Reqwest(error)) => { assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) @@ -207,4 +227,60 @@ mod tests { assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); } + + #[tokio::test] + async fn list() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let urls = vec![ + format!("pubky://{}/pub/a.wrong/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.wrong/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{}/pub/z.wrong/a.txt", keypair.public_key()), + ]; + + for url in urls { + client.put(url.as_str(), &[0]).await.unwrap(); + } + + { + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let list = client.list(url.as_str(), false).await.unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + ] + ); + } + + { + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let list = client.list(url.as_str(), true).await.unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + ] + ); + } + } } From 01cc1e91bbab833d340748ef405904d539541281 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 4 Aug 2024 16:38:40 +0300 Subject: [PATCH 056/125] feat(pubky): add list limit --- .../src/database/tables/entries.rs | 2 +- pubky/src/shared/public.rs | 50 ++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 75aefff..0e82ed3 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -96,7 +96,7 @@ impl DB { let txn = self.env.read_txn()?; let prefix = format!("{public_key}/{prefix}"); - let limit = limit.unwrap_or(MAX_LIST_LIMIT).max(MAX_LIST_LIMIT); + let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); // Vector to store results let mut results = Vec::new(); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index c66af64..cfc315c 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -53,15 +53,27 @@ impl PubkyClient { Ok(()) } - pub async fn list>(&self, url: T, reverse: bool) -> Result> { + pub async fn list>( + &self, + url: T, + reverse: bool, + limit: Option, + ) -> Result> { let mut url = self.pubky_to_http(url).await?; + let mut query = url.query_pairs_mut(); + query.append_key_only("list"); + if reverse { - url.set_query("list&reverse".into()); - } else { - url.set_query("list".into()); + query.append_key_only("reverse"); } + if let Some(limit) = limit { + query.append_pair("limit", &limit.to_string()); + } + + drop(query); + let response = self.request(Method::GET, url).send().await?; response.error_for_status_ref()?; @@ -255,7 +267,7 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), false).await.unwrap(); + let list = client.list(url.as_str(), false, None).await.unwrap(); assert_eq!( list, @@ -270,7 +282,20 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), true).await.unwrap(); + let list = client.list(url.as_str(), false, Some(2)).await.unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + ] + ); + } + + { + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let list = client.list(url.as_str(), true, None).await.unwrap(); assert_eq!( list, @@ -282,5 +307,18 @@ mod tests { ] ); } + + { + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let list = client.list(url.as_str(), true, Some(2)).await.unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + ] + ); + } } } From ed4afeea609319d25a1638b787ec73920c05a7b9 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 4 Aug 2024 17:27:24 +0300 Subject: [PATCH 057/125] feat(pubky): add list cursor --- .../src/database/tables/entries.rs | 50 +++++++++-- pubky-homeserver/src/routes/public.rs | 1 + pubky/src/shared/public.rs | 90 +++++++++++++++++-- 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 0e82ed3..e708046 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -91,11 +91,23 @@ impl DB { prefix: &str, reverse: bool, limit: Option, + cursor: Option, ) -> anyhow::Result> { let db = self.tables.entries; let txn = self.env.read_txn()?; let prefix = format!("{public_key}/{prefix}"); + // Normalized cursor + let cursor = cursor.map(|mut cursor| { + if cursor.starts_with("pubky://") { + cursor = cursor[8..].into(); + } + if cursor.starts_with(&prefix) { + cursor + } else { + format!("{prefix}{cursor}") + } + }); let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); // Vector to store results @@ -103,22 +115,44 @@ impl DB { // Fetch data based on direction if reverse { - let mut iter = self.tables.entries.rev_prefix_iter(&txn, &prefix)?; + if let Some(x) = &cursor { + let mut cursor = cursor.unwrap_or(prefix.to_string()); + let mut cursor = cursor.as_str(); - for _ in 0..limit { - if let Some((key, _)) = iter.next().transpose()? { - results.push(format!("pubky://{}", key)) - }; + for _ in 0..limit { + if let Some((key, _)) = self.tables.entries.get_lower_than(&txn, cursor)? { + if !key.starts_with(&prefix) { + break; + } + cursor = key; + results.push(format!("pubky://{}", key)) + }; + } + } else { + // TODO: find a way to avoid this special case. + + let mut iter = self.tables.entries.rev_prefix_iter(&txn, &prefix)?; + + for _ in 0..limit { + if let Some((key, _)) = iter.next().transpose()? { + results.push(format!("pubky://{}", key)) + }; + } } } else { - let mut iter = self.tables.entries.prefix_iter(&txn, &prefix)?; + let mut cursor = cursor.unwrap_or(prefix.to_string()); + let mut cursor = cursor.as_str(); for _ in 0..limit { - if let Some((key, _)) = iter.next().transpose()? { + if let Some((key, _)) = self.tables.entries.get_greater_than(&txn, cursor)? { + if !key.starts_with(&prefix) { + break; + } + cursor = key; results.push(format!("pubky://{}", key)) }; } - } + }; Ok(results) } diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 79a1637..9e4a6b4 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -89,6 +89,7 @@ pub async fn get( path.as_str(), params.contains_key("reverse"), params.get("limit").and_then(|l| l.parse::().ok()), + params.get("cursor").map(|cursor| cursor.into()), )?; return Ok(vec.join("\n").into()); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index cfc315c..4782a6c 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -58,6 +58,7 @@ impl PubkyClient { url: T, reverse: bool, limit: Option, + cursor: Option<&str>, ) -> Result> { let mut url = self.pubky_to_http(url).await?; @@ -72,6 +73,10 @@ impl PubkyClient { query.append_pair("limit", &limit.to_string()); } + if let Some(cursor) = cursor { + query.append_pair("cursor", cursor); + } + drop(query); let response = self.request(Method::GET, url).send().await?; @@ -267,7 +272,7 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), false, None).await.unwrap(); + let list = client.list(url.as_str(), false, None, None).await.unwrap(); assert_eq!( list, @@ -276,26 +281,73 @@ mod tests { format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - ] + ], + "normal list with no limit or cursor" ); } { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), false, Some(2)).await.unwrap(); + let list = client + .list(url.as_str(), false, Some(2), None) + .await + .unwrap(); assert_eq!( list, vec![ format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - ] + ], + "normal list with limit but no cursor" ); } { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), true, None).await.unwrap(); + let list = client + .list(url.as_str(), false, Some(2), Some("a.txt")) + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + ], + "normal list with limit and a suffix cursor" + ); + } + + { + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let list = client + .list( + url.as_str(), + false, + Some(2), + Some(&format!( + "pubky://{}/pub/example.com/a.txt", + keypair.public_key() + )), + ) + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + ], + "normal list with limit and a full url cursor" + ); + } + + { + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let list = client.list(url.as_str(), true, None, None).await.unwrap(); assert_eq!( list, @@ -304,20 +356,42 @@ mod tests { format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - ] + ], + "reverse list with no limit or cursor" ); } { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), true, Some(2)).await.unwrap(); + let list = client + .list(url.as_str(), true, Some(2), None) + .await + .unwrap(); assert_eq!( list, vec![ format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - ] + ], + "reverse list with limit but no cursor" + ); + } + + { + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let list = client + .list(url.as_str(), true, Some(2), Some("d.txt")) + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + ], + "reverse list with limit and cursor" ); } } From 38d9c98caad067b682e4b5d2083a8babb3a9338f Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 5 Aug 2024 12:16:36 +0300 Subject: [PATCH 058/125] feat(pubky): use builder pattern for client::list() --- .../src/database/tables/entries.rs | 4 +- pubky-homeserver/src/routes/public.rs | 2 +- pubky/src/error.rs | 3 + pubky/src/native.rs | 9 +- pubky/src/shared/list_builder.rs | 73 +++++++++++++ pubky/src/shared/mod.rs | 1 + pubky/src/shared/public.rs | 101 ++++++++---------- 7 files changed, 132 insertions(+), 61 deletions(-) create mode 100644 pubky/src/shared/list_builder.rs diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index e708046..0d8fa35 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -20,7 +20,7 @@ pub type EntriesTable = Database; pub const ENTRIES_TABLE: &str = "entries"; -const MAX_LIST_LIMIT: i32 = 100; +const MAX_LIST_LIMIT: u16 = 100; impl DB { pub fn put_entry( @@ -90,7 +90,7 @@ impl DB { public_key: &PublicKey, prefix: &str, reverse: bool, - limit: Option, + limit: Option, cursor: Option, ) -> anyhow::Result> { let db = self.tables.entries; diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 9e4a6b4..26d270e 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -88,7 +88,7 @@ pub async fn get( public_key, path.as_str(), params.contains_key("reverse"), - params.get("limit").and_then(|l| l.parse::().ok()), + params.get("limit").and_then(|l| l.parse::().ok()), params.get("cursor").map(|cursor| cursor.into()), )?; diff --git a/pubky/src/error.rs b/pubky/src/error.rs index e27814b..40eca3b 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -28,6 +28,9 @@ pub enum Error { #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")] RecoverFileInvalidSecretKeyLength(usize), + #[error("Could not convert the passed type into a Url")] + InvalidUrl, + // === Transparent === #[error(transparent)] Dns(#[from] SimpleDnsError), diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 6fc3e72..8fb6ee0 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -12,7 +12,10 @@ use url::Url; use crate::{ error::Result, - shared::recovery_file::{create_recovery_file, decrypt_recovery_file}, + shared::{ + list_builder::ListBuilder, + recovery_file::{create_recovery_file, decrypt_recovery_file}, + }, PubkyClient, }; @@ -104,6 +107,10 @@ impl PubkyClient { self.inner_delete(url).await } + pub fn list>(&self, url: T) -> Result { + self.inner_list(url) + } + // === Helpers === /// Create a recovery file of the `keypair`, containing the secret key encrypted diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs new file mode 100644 index 0000000..a1a1330 --- /dev/null +++ b/pubky/src/shared/list_builder.rs @@ -0,0 +1,73 @@ +use reqwest::{Method, Response, StatusCode}; +use url::Url; + +use crate::{error::Result, PubkyClient}; + +#[derive(Debug)] +pub struct ListBuilder<'a> { + url: Url, + reverse: bool, + limit: Option, + cursor: Option<&'a str>, + client: &'a PubkyClient, +} + +impl<'a> ListBuilder<'a> { + pub fn new(client: &'a PubkyClient, url: Url) -> Self { + Self { + client, + url, + limit: None, + cursor: None, + reverse: false, + } + } + + pub fn reverse(mut self, reverse: bool) -> Self { + self.reverse = reverse; + self + } + + pub fn limit(mut self, limit: u16) -> Self { + self.limit = limit.into(); + self + } + + pub fn cursor(mut self, cursor: &'a str) -> Self { + self.cursor = cursor.into(); + self + } + + pub async fn send(self) -> Result> { + let mut url = self.client.pubky_to_http(self.url).await?; + + let mut query = url.query_pairs_mut(); + query.append_key_only("list"); + + if self.reverse { + query.append_key_only("reverse"); + } + + if let Some(limit) = self.limit { + query.append_pair("limit", &limit.to_string()); + } + + if let Some(cursor) = self.cursor { + query.append_pair("cursor", cursor); + } + + drop(query); + + let response = self.client.request(Method::GET, url).send().await?; + + response.error_for_status_ref()?; + + // TODO: bail on too large files. + let bytes = response.bytes().await?; + + Ok(String::from_utf8_lossy(&bytes) + .lines() + .map(String::from) + .collect()) + } +} diff --git a/pubky/src/shared/mod.rs b/pubky/src/shared/mod.rs index 49f11bd..550cc6e 100644 --- a/pubky/src/shared/mod.rs +++ b/pubky/src/shared/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod list_builder; pub mod pkarr; pub mod public; pub mod recovery_file; diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 4782a6c..e1eaed8 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -9,7 +9,7 @@ use crate::{ PubkyClient, }; -use super::pkarr::Endpoint; +use super::{list_builder::ListBuilder, pkarr::Endpoint}; impl PubkyClient { pub async fn inner_put>(&self, url: T, content: &[u8]) -> Result<()> { @@ -53,49 +53,15 @@ impl PubkyClient { Ok(()) } - pub async fn list>( - &self, - url: T, - reverse: bool, - limit: Option, - cursor: Option<&str>, - ) -> Result> { - let mut url = self.pubky_to_http(url).await?; - - let mut query = url.query_pairs_mut(); - query.append_key_only("list"); - - if reverse { - query.append_key_only("reverse"); - } - - if let Some(limit) = limit { - query.append_pair("limit", &limit.to_string()); - } - - if let Some(cursor) = cursor { - query.append_pair("cursor", cursor); - } - - drop(query); - - let response = self.request(Method::GET, url).send().await?; - - response.error_for_status_ref()?; - - // TODO: bail on too large files. - let bytes = response.bytes().await?; - - Ok(String::from_utf8_lossy(&bytes) - .lines() - .map(String::from) - .collect()) + pub fn inner_list>(&self, url: T) -> Result { + Ok(ListBuilder::new( + self, + url.try_into().map_err(|_| Error::InvalidUrl)?, + )) } - async fn pubky_to_http>(&self, url: T) -> Result { - let mut original_url: Url = url - .try_into() - .map_err(|e| Error::Generic("Invalid Url".to_string()))?; + pub(crate) async fn pubky_to_http>(&self, url: T) -> Result { + let mut original_url: Url = url.try_into().map_err(|_| Error::InvalidUrl)?; if original_url.scheme() != "pubky" { return Ok(original_url); @@ -272,7 +238,7 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), false, None, None).await.unwrap(); + let list = client.list(url.as_str()).unwrap().send().await.unwrap(); assert_eq!( list, @@ -289,7 +255,10 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client - .list(url.as_str(), false, Some(2), None) + .list(url.as_str()) + .unwrap() + .limit(2) + .send() .await .unwrap(); @@ -306,7 +275,11 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client - .list(url.as_str(), false, Some(2), Some("a.txt")) + .list(url.as_str()) + .unwrap() + .limit(2) + .cursor("a.txt") + .send() .await .unwrap(); @@ -323,15 +296,14 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client - .list( - url.as_str(), - false, - Some(2), - Some(&format!( - "pubky://{}/pub/example.com/a.txt", - keypair.public_key() - )), - ) + .list(url.as_str()) + .unwrap() + .limit(2) + .cursor(&format!( + "pubky://{}/pub/example.com/a.txt", + keypair.public_key() + )) + .send() .await .unwrap(); @@ -347,7 +319,13 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); - let list = client.list(url.as_str(), true, None, None).await.unwrap(); + let list = client + .list(url.as_str()) + .unwrap() + .reverse(true) + .send() + .await + .unwrap(); assert_eq!( list, @@ -364,7 +342,11 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client - .list(url.as_str(), true, Some(2), None) + .list(url.as_str()) + .unwrap() + .reverse(true) + .limit(2) + .send() .await .unwrap(); @@ -381,7 +363,12 @@ mod tests { { let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client - .list(url.as_str(), true, Some(2), Some("d.txt")) + .list(url.as_str()) + .unwrap() + .reverse(true) + .limit(2) + .cursor("d.txt") + .send() .await .unwrap(); From 1db1d3a21d76880cc9064ae93c7bfe0e26f386c3 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 5 Aug 2024 16:34:26 +0300 Subject: [PATCH 059/125] feat(js): add list() method --- pubky/Cargo.toml | 4 +- pubky/pkg/test/public.js | 132 +++++++++++++++++++++++++++++++ pubky/src/native.rs | 3 + pubky/src/shared/list_builder.rs | 11 +++ pubky/src/shared/public.rs | 9 +-- pubky/src/wasm.rs | 62 +++++++++++++-- 6 files changed, 207 insertions(+), 14 deletions(-) diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 149b6be..4c82cfb 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -40,5 +40,5 @@ tokio = "1.37.0" [package.metadata.docs.rs] all-features = true -# [package.metadata.wasm-pack.profile.release] -# wasm-opt = ['-g', '-O'] +[package.metadata.wasm-pack.profile.release] +wasm-opt = ['-g', '-O'] diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 2e6c988..9ceaa5b 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -111,3 +111,135 @@ test("forbidden", async (t) => { `HTTP status client error (403 Forbidden) for url (http://localhost:15411/${publicKey.z32()}/priv/example.com/arbitrary)` ) }) + +test("list", async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.publicKey() + const pubky = publicKey.z32() + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + await client.signup(keypair, homeserver) + + + + let urls = [ + `pubky://${pubky}/pub/a.wrong/a.txt`, + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.wrong/a.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/z.wrong/a.txt`, + ] + + for (let url of urls) { + await client.put(url, Buffer.from("")); + } + + let url = `pubky://${pubky}/pub/example.com/`; + + { + let list = await client.list(url); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/d.txt`, + + ], + "normal list with no limit or cursor" + ); + } + + { + let list = await client.list(url, null, null, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + + ], + "normal list with limit but no cursor" + ); + } + + { + let list = await client.list(url, "a.txt", null, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + + ], + "normal list with limit and a suffix cursor" + ); + } + + { + let list = await client.list(url, `pubky://${pubky}/pub/example.com/a.txt`, null, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + + ], + "normal list with limit and a full url cursor" + ); + } + + + { + let list = await client.list(url, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/a.txt`, + + ], + "reverse list with no limit or cursor" + ); + } + + { + let list = await client.list(url, null, true, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + + ], + "reverse list with limit but no cursor" + ); + } + + { + let list = await client.list(url, "d.txt", true, 2); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + + ], + "reverse list with limit and a suffix cursor" + ); + } +}) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 8fb6ee0..6f9f676 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -107,6 +107,9 @@ impl PubkyClient { self.inner_delete(url).await } + /// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send]. + /// + /// `url` sets the path you want to lest within. pub fn list>(&self, url: T) -> Result { self.inner_list(url) } diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs index a1a1330..5d0aa32 100644 --- a/pubky/src/shared/list_builder.rs +++ b/pubky/src/shared/list_builder.rs @@ -13,6 +13,7 @@ pub struct ListBuilder<'a> { } impl<'a> ListBuilder<'a> { + /// Create a new List request builder pub fn new(client: &'a PubkyClient, url: Url) -> Self { Self { client, @@ -23,21 +24,31 @@ impl<'a> ListBuilder<'a> { } } + /// Set the `reverse` option. pub fn reverse(mut self, reverse: bool) -> Self { self.reverse = reverse; self } + /// Set the `limit` value. pub fn limit(mut self, limit: u16) -> Self { self.limit = limit.into(); self } + /// Set the `cursor` value. + /// + /// usually the last url from previous responses. pub fn cursor(mut self, cursor: &'a str) -> Self { self.cursor = cursor.into(); self } + /// Send the list request. + /// + /// Returns a list of Pubky URLs of the files in the path of the `url` + /// respecting [ListBuilder::reverse], [ListBuilder::limit] and [ListBuilder::cursor] + /// options. pub async fn send(self) -> Result> { let mut url = self.client.pubky_to_http(self.url).await?; diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index e1eaed8..f1b5829 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -236,8 +236,9 @@ mod tests { client.put(url.as_str(), &[0]).await.unwrap(); } + let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + { - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client.list(url.as_str()).unwrap().send().await.unwrap(); assert_eq!( @@ -253,7 +254,6 @@ mod tests { } { - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client .list(url.as_str()) .unwrap() @@ -273,7 +273,6 @@ mod tests { } { - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client .list(url.as_str()) .unwrap() @@ -294,7 +293,6 @@ mod tests { } { - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client .list(url.as_str()) .unwrap() @@ -318,7 +316,6 @@ mod tests { } { - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client .list(url.as_str()) .unwrap() @@ -340,7 +337,6 @@ mod tests { } { - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client .list(url.as_str()) .unwrap() @@ -361,7 +357,6 @@ mod tests { } { - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); let list = client .list(url.as_str()) .unwrap() diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 7f44e44..2e54332 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -3,7 +3,8 @@ use std::{ sync::{Arc, RwLock}, }; -use wasm_bindgen::prelude::*; +use js_sys::{Array, Uint8Array}; +use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; use reqwest::{IntoUrl, Method, RequestBuilder, Response}; use url::Url; @@ -58,7 +59,7 @@ impl PubkyClient { pub fn create_recovery_file( keypair: &Keypair, passphrase: &str, - ) -> Result { + ) -> Result { create_recovery_file(keypair.as_inner(), passphrase) .map(|b| b.as_slice().into()) .map_err(|e| e.into()) @@ -138,18 +139,69 @@ impl PubkyClient { self.inner_put(url, content).await.map_err(|e| e.into()) } - #[wasm_bindgen] /// Download a small payload from a given path relative to a pubky author. - pub async fn get(&self, url: &str) -> Result, JsValue> { + #[wasm_bindgen] + pub async fn get(&self, url: &str) -> Result, JsValue> { self.inner_get(url) .await .map(|b| b.map(|b| (&*b).into())) .map_err(|e| e.into()) } - #[wasm_bindgen] /// Delete a file at a path relative to a pubky author. + #[wasm_bindgen] pub async fn delete(&self, url: &str) -> Result<(), JsValue> { self.inner_delete(url).await.map_err(|e| e.into()) } + + /// Returns a list of Pubky URLs of the files within the `url` path, + /// respecting the `cursor`, `reverse` and `limit` options. + /// + /// `cursor` is usually the last url from previous responses. + #[wasm_bindgen] + pub async fn list( + &self, + url: &str, + cursor: Option, + reverse: Option, + limit: Option, + ) -> Result { + // TODO: try later to return Vec from async function. + + if let Some(cursor) = cursor { + return self + .inner_list(url)? + .reverse(reverse.unwrap_or(false)) + .limit(limit.unwrap_or(u16::MAX)) + .cursor(&cursor) + .send() + .await + .map(|urls| { + let js_array = Array::new(); + + for url in urls { + js_array.push(&JsValue::from_str(&url)); + } + + js_array + }) + .map_err(|e| e.into()); + } + + self.inner_list(url)? + .reverse(reverse.unwrap_or(false)) + .limit(limit.unwrap_or(u16::MAX)) + .send() + .await + .map(|urls| { + let js_array = Array::new(); + + for url in urls { + js_array.push(&JsValue::from_str(&url)); + } + + js_array + }) + .map_err(|e| e.into()) + } } From b26fb51785fdcf95d32c1ae3b5b28a7eebd768fb Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 5 Aug 2024 16:42:00 +0300 Subject: [PATCH 060/125] docs(js): update README with API documentation --- pubky/pkg/README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index c2f89d0..2bf2c2b 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -110,10 +110,20 @@ let response = await client.get(url) ### delete ```js -let response = await delete(url); +let response = await client.delete(url); ``` - url: A string representing the Pubky URL. +### list +```js +let response = await client.list(url, cursor, reverse, limit) +``` +- url: A string representing the Pubky URL. The path in that url is the prefix that you want to list files within. +- cursor: Usually the last URL from previous calls. List urls after/before (depending on `reverse`) the cursor. +- reverse: Whether or not return urls in reverse order. +- limit: Number of urls to return. +- Returns: A list of URLs of the files in the `url` you passed. + ### Keypair #### random @@ -186,5 +196,5 @@ Use the logged addresses as inputs to `PubkyClient` ```js import { PubkyClient } from '../index.js' -const client = new PubkyClient().testnet(); +const client = PubkyClient().testnet(); ``` From 639ed2d131c2eaa15be1f3fcd27762103f5acd53 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 5 Aug 2024 16:46:09 +0300 Subject: [PATCH 061/125] chore(js): publish 0.1.12 --- pubky/pkg/README.md | 4 ++++ pubky/pkg/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 2bf2c2b..3fcbcd1 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -14,6 +14,10 @@ JavaScript implementation of [Pubky](https://github.com/pubky/pubky). npm install @synonymdev/pubky ``` +### Prerequisites + +For Nodejs, you need Node v20 or later. + ## Getting started ```js diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index c66153e..4bce927 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.10", + "version": "0.1.12", "license": "MIT", "repository": { "type": "git", From 59360d9cd6cee33a82fcb5ff2b618b2101acf37c Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 6 Aug 2024 10:57:36 +0300 Subject: [PATCH 062/125] feat(homeserver): return list results as json newline --- pubky-homeserver/src/routes/public.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 26d270e..a99f633 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -4,7 +4,7 @@ use axum::{ body::{Body, Bytes}, debug_handler, extract::{Path, Query, State}, - http::StatusCode, + http::{header, Response, StatusCode}, response::IntoResponse, RequestExt, Router, }; @@ -92,14 +92,18 @@ pub async fn get( params.get("cursor").map(|cursor| cursor.into()), )?; - return Ok(vec.join("\n").into()); + return Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from(vec.join("\n"))) + .unwrap()); } // TODO: Enable streaming match state.db.get_blob(public_key, path.as_str()) { Err(error) => Err(error)?, - Ok(Some(bytes)) => Ok(bytes), + Ok(Some(bytes)) => Ok(Response::builder().body(Body::from(bytes)).unwrap()), Ok(None) => Err(Error::with_status(StatusCode::NOT_FOUND)), } } From dd2c9ea93ccf5fe25595ede67cfddaa3d04e8685 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 7 Aug 2024 18:01:02 +0300 Subject: [PATCH 063/125] feat(homeserver): list when path ends with '/' and some refactoring --- .../src/database/tables/entries.rs | 71 +++++++------------ pubky-homeserver/src/routes/public.rs | 24 +++++-- pubky/src/shared/list_builder.rs | 11 ++- pubky/src/shared/public.rs | 2 +- 4 files changed, 56 insertions(+), 52 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 0d8fa35..67d626b 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, fmt::Result, time::SystemTime}; use heed::{ types::{Bytes, Str}, - BoxedError, BytesDecode, BytesEncode, Database, + BoxedError, BytesDecode, BytesEncode, Database, RoTxn, }; use pubky_common::{ @@ -82,32 +82,31 @@ impl DB { Ok(deleted) } + pub fn contains_directory(&self, txn: &RoTxn, path: &str) -> anyhow::Result { + Ok(self.tables.entries.get_greater_than(txn, path)?.is_some()) + } + /// Return a list of pubky urls. /// /// - limit defaults to and capped by [MAX_LIST_LIMIT] pub fn list( &self, - public_key: &PublicKey, - prefix: &str, + txn: &RoTxn, + path: &str, reverse: bool, limit: Option, cursor: Option, + shallow: bool, ) -> anyhow::Result> { - let db = self.tables.entries; - let txn = self.env.read_txn()?; + // Remove directories from the cursor; + let cursor = cursor + .as_deref() + .and_then(|mut cursor| cursor.rsplit('/').next()) + .unwrap_or(if reverse { "~" } else { "" }); + + let cursor = format!("{path}{cursor}"); + let mut cursor = cursor.as_str(); - let prefix = format!("{public_key}/{prefix}"); - // Normalized cursor - let cursor = cursor.map(|mut cursor| { - if cursor.starts_with("pubky://") { - cursor = cursor[8..].into(); - } - if cursor.starts_with(&prefix) { - cursor - } else { - format!("{prefix}{cursor}") - } - }); let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); // Vector to store results @@ -115,37 +114,19 @@ impl DB { // Fetch data based on direction if reverse { - if let Some(x) = &cursor { - let mut cursor = cursor.unwrap_or(prefix.to_string()); - let mut cursor = cursor.as_str(); - - for _ in 0..limit { - if let Some((key, _)) = self.tables.entries.get_lower_than(&txn, cursor)? { - if !key.starts_with(&prefix) { - break; - } - cursor = key; - results.push(format!("pubky://{}", key)) - }; - } - } else { - // TODO: find a way to avoid this special case. - - let mut iter = self.tables.entries.rev_prefix_iter(&txn, &prefix)?; - - for _ in 0..limit { - if let Some((key, _)) = iter.next().transpose()? { - results.push(format!("pubky://{}", key)) - }; - } + for _ in 0..limit { + if let Some((key, _)) = self.tables.entries.get_lower_than(txn, cursor)? { + if !key.starts_with(path) { + break; + } + cursor = key; + results.push(format!("pubky://{}", key)) + }; } } else { - let mut cursor = cursor.unwrap_or(prefix.to_string()); - let mut cursor = cursor.as_str(); - for _ in 0..limit { - if let Some((key, _)) = self.tables.entries.get_greater_than(&txn, cursor)? { - if !key.starts_with(&prefix) { + if let Some((key, _)) = self.tables.entries.get_greater_than(txn, cursor)? { + if !key.starts_with(path) { break; } cursor = key; diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index a99f633..33a6da3 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -82,14 +82,28 @@ pub async fn get( verify(path.as_str()); let public_key = pubky.public_key(); - if params.contains_key("list") { + let path = path.as_str(); + + if path.ends_with('/') { + let txn = state.db.env.read_txn()?; + + let path = format!("{public_key}/{path}"); + + if !state.db.contains_directory(&txn, &path)? { + return Err(Error::new( + StatusCode::NOT_FOUND, + "Directory Not Found".into(), + )); + } + // Handle listing let vec = state.db.list( - public_key, - path.as_str(), + &txn, + &path, params.contains_key("reverse"), params.get("limit").and_then(|l| l.parse::().ok()), params.get("cursor").map(|cursor| cursor.into()), + params.contains_key("shallow"), )?; return Ok(Response::builder() @@ -101,10 +115,10 @@ pub async fn get( // TODO: Enable streaming - match state.db.get_blob(public_key, path.as_str()) { + match state.db.get_blob(public_key, path) { Err(error) => Err(error)?, Ok(Some(bytes)) => Ok(Response::builder().body(Body::from(bytes)).unwrap()), - Ok(None) => Err(Error::with_status(StatusCode::NOT_FOUND)), + Ok(None) => Err(Error::new(StatusCode::NOT_FOUND, "File Not Found".into())), } } diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs index 5d0aa32..4d63351 100644 --- a/pubky/src/shared/list_builder.rs +++ b/pubky/src/shared/list_builder.rs @@ -52,8 +52,17 @@ impl<'a> ListBuilder<'a> { pub async fn send(self) -> Result> { let mut url = self.client.pubky_to_http(self.url).await?; + if !url.path().ends_with('/') { + let path = url.path().to_string(); + let mut parts = path.split('/').collect::>(); + parts.pop(); + + let path = format!("{}/", parts.join("/")); + + url.set_path(&path) + } + let mut query = url.query_pairs_mut(); - query.append_key_only("list"); if self.reverse { query.append_key_only("reverse"); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index f1b5829..a316306 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -236,7 +236,7 @@ mod tests { client.put(url.as_str(), &[0]).await.unwrap(); } - let url = format!("pubky://{}/pub/example.com/", keypair.public_key()); + let url = format!("pubky://{}/pub/example.com/extra", keypair.public_key()); { let list = client.list(url.as_str()).unwrap().send().await.unwrap(); From e10cf77fe26296c5fbcbbfef5e0a72c211897fdc Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 7 Aug 2024 18:52:04 +0300 Subject: [PATCH 064/125] feat(pubky): shallow list folder and files --- .../src/database/tables/entries.rs | 69 ++++++++++++--- pubky/src/shared/list_builder.rs | 15 +++- pubky/src/shared/public.rs | 86 ++++++++++++++++++- 3 files changed, 152 insertions(+), 18 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 67d626b..af26828 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -98,39 +98,82 @@ impl DB { cursor: Option, shallow: bool, ) -> anyhow::Result> { + // Vector to store results + let mut results = Vec::new(); + + let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); + // Remove directories from the cursor; let cursor = cursor .as_deref() .and_then(|mut cursor| cursor.rsplit('/').next()) .unwrap_or(if reverse { "~" } else { "" }); - let cursor = format!("{path}{cursor}"); - let mut cursor = cursor.as_str(); - - let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); - - // Vector to store results - let mut results = Vec::new(); + let mut cursor = format!("{path}{cursor}"); // Fetch data based on direction if reverse { for _ in 0..limit { - if let Some((key, _)) = self.tables.entries.get_lower_than(txn, cursor)? { + if let Some((key, _)) = self.tables.entries.get_lower_than(txn, &cursor)? { if !key.starts_with(path) { break; } - cursor = key; - results.push(format!("pubky://{}", key)) + + if shallow { + let mut split = key[path.len()..].split('/'); + let item = split.next().expect("should not be reachable"); + + let is_directory = split.next().is_some(); + + cursor = format!( + "{}{}", + &key[..(path.len() + item.len())], + // `.` is immediately lower than `/` + if is_directory { "." } else { "" } + ); + + let url = format!( + "pubky://{path}{item}{}", + if is_directory { "/" } else { "" } + ); + + results.push(url); + } else { + cursor = key.to_string(); + results.push(format!("pubky://{}", key)) + } }; } } else { for _ in 0..limit { - if let Some((key, _)) = self.tables.entries.get_greater_than(txn, cursor)? { + if let Some((key, _)) = self.tables.entries.get_greater_than(txn, &cursor)? { if !key.starts_with(path) { break; } - cursor = key; - results.push(format!("pubky://{}", key)) + + if shallow { + let mut split = key[path.len()..].split('/'); + let item = split.next().expect("should not be reachable"); + + let is_directory = split.next().is_some(); + + cursor = format!( + "{}{}", + &key[..(path.len() + item.len())], + // `0` is immediately higher than `/` + if is_directory { "0" } else { "" } + ); + + let url = format!( + "pubky://{path}{item}{}", + if is_directory { "/" } else { "" } + ); + + results.push(url); + } else { + cursor = key.to_string(); + results.push(format!("pubky://{}", key)) + } }; } }; diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs index 4d63351..5eccc7e 100644 --- a/pubky/src/shared/list_builder.rs +++ b/pubky/src/shared/list_builder.rs @@ -10,6 +10,7 @@ pub struct ListBuilder<'a> { limit: Option, cursor: Option<&'a str>, client: &'a PubkyClient, + shallow: bool, } impl<'a> ListBuilder<'a> { @@ -21,12 +22,13 @@ impl<'a> ListBuilder<'a> { limit: None, cursor: None, reverse: false, + shallow: false, } } /// Set the `reverse` option. - pub fn reverse(mut self, reverse: bool) -> Self { - self.reverse = reverse; + pub fn reverse(mut self) -> Self { + self.reverse = true; self } @@ -44,6 +46,11 @@ impl<'a> ListBuilder<'a> { self } + pub fn shallow(mut self) -> Self { + self.shallow = true; + self + } + /// Send the list request. /// /// Returns a list of Pubky URLs of the files in the path of the `url` @@ -68,6 +75,10 @@ impl<'a> ListBuilder<'a> { query.append_key_only("reverse"); } + if self.shallow { + query.append_key_only("shallow"); + } + if let Some(limit) = self.limit { query.append_pair("limit", &limit.to_string()); } diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index a316306..0f74874 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -319,7 +319,7 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .reverse(true) + .reverse() .send() .await .unwrap(); @@ -340,7 +340,7 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .reverse(true) + .reverse() .limit(2) .send() .await @@ -360,7 +360,7 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .reverse(true) + .reverse() .limit(2) .cursor("d.txt") .send() @@ -377,4 +377,84 @@ mod tests { ); } } + + #[tokio::test] + async fn list_shallow() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let urls = vec![ + format!("pubky://{}/pub/a.com/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{}/pub/example.con/d.txt", keypair.public_key()), + format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{}/pub/file", keypair.public_key()), + format!("pubky://{}/pub/file2", keypair.public_key()), + format!("pubky://{}/pub/z.com/a.txt", keypair.public_key()), + ]; + + for url in urls { + client.put(url.as_str(), &[0]).await.unwrap(); + } + + let url = format!("pubky://{}/pub/", keypair.public_key()); + + { + let list = client + .list(url.as_str()) + .unwrap() + .shallow() + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/a.com/", keypair.public_key()), + format!("pubky://{}/pub/example.com/", keypair.public_key()), + format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{}/pub/example.con/", keypair.public_key()), + format!("pubky://{}/pub/file", keypair.public_key()), + format!("pubky://{}/pub/file2", keypair.public_key()), + format!("pubky://{}/pub/z.com/", keypair.public_key()), + ], + "normal list shallow" + ); + } + + { + let list = client + .list(url.as_str()) + .unwrap() + .shallow() + .reverse() + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/z.com/", keypair.public_key()), + format!("pubky://{}/pub/file2", keypair.public_key()), + format!("pubky://{}/pub/file", keypair.public_key()), + format!("pubky://{}/pub/example.con/", keypair.public_key()), + format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{}/pub/example.com/", keypair.public_key()), + format!("pubky://{}/pub/a.com/", keypair.public_key()), + ], + "reverse list shallow" + ); + } + } } From 5eb54402d25a4444d7f3f26630635f4a80f58edb Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 7 Aug 2024 19:05:29 +0300 Subject: [PATCH 065/125] feat(js): add list shallow option --- pubky/pkg/test/public.js | 68 ++++++++++++++++++++++++++++++++ pubky/src/shared/list_builder.rs | 8 ++-- pubky/src/shared/public.rs | 12 +++--- pubky/src/wasm.rs | 3 ++ 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 9ceaa5b..4b301e7 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -243,3 +243,71 @@ test("list", async (t) => { ); } }) + +test('list shallow', async (t) => { + const client = PubkyClient.testnet(); + + const keypair = Keypair.random() + const publicKey = keypair.publicKey() + const pubky = publicKey.z32() + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + await client.signup(keypair, homeserver) + + + + let urls = [ + `pubky://${pubky}/pub/a.com/a.txt`, + `pubky://${pubky}/pub/example.com/a.txt`, + `pubky://${pubky}/pub/example.com/b.txt`, + `pubky://${pubky}/pub/example.com/c.txt`, + `pubky://${pubky}/pub/example.com/d.txt`, + `pubky://${pubky}/pub/example.con/d.txt`, + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/z.com/a.txt`, + ] + + for (let url of urls) { + await client.put(url, Buffer.from("")); + } + + let url = `pubky://${pubky}/pub/`; + + { + let list = await client.list(url, null, false, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/a.com/`, + `pubky://${pubky}/pub/example.com/`, + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/example.con/`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/z.com/`, + ], + "normal list shallow" + ); + } + + { + let list = await client.list(url, null, true, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/z.com/`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/example.con/`, + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/example.com/`, + `pubky://${pubky}/pub/a.com/`, + ], + "normal list shallow" + ); + } +}) diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs index 5eccc7e..8c5cde0 100644 --- a/pubky/src/shared/list_builder.rs +++ b/pubky/src/shared/list_builder.rs @@ -27,8 +27,8 @@ impl<'a> ListBuilder<'a> { } /// Set the `reverse` option. - pub fn reverse(mut self) -> Self { - self.reverse = true; + pub fn reverse(mut self, reverse: bool) -> Self { + self.reverse = reverse; self } @@ -46,8 +46,8 @@ impl<'a> ListBuilder<'a> { self } - pub fn shallow(mut self) -> Self { - self.shallow = true; + pub fn shallow(mut self, shallow: bool) -> Self { + self.shallow = shallow; self } diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 0f74874..9bd099f 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -319,7 +319,7 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .reverse() + .reverse(true) .send() .await .unwrap(); @@ -340,7 +340,7 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .reverse() + .reverse(true) .limit(2) .send() .await @@ -360,7 +360,7 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .reverse() + .reverse(true) .limit(2) .cursor("d.txt") .send() @@ -412,7 +412,7 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .shallow() + .shallow(true) .send() .await .unwrap(); @@ -436,8 +436,8 @@ mod tests { let list = client .list(url.as_str()) .unwrap() - .shallow() - .reverse() + .shallow(true) + .reverse(true) .send() .await .unwrap(); diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 2e54332..07b6749 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -165,6 +165,7 @@ impl PubkyClient { cursor: Option, reverse: Option, limit: Option, + shallow: Option, ) -> Result { // TODO: try later to return Vec from async function. @@ -174,6 +175,7 @@ impl PubkyClient { .reverse(reverse.unwrap_or(false)) .limit(limit.unwrap_or(u16::MAX)) .cursor(&cursor) + .shallow(shallow.unwrap_or(false)) .send() .await .map(|urls| { @@ -191,6 +193,7 @@ impl PubkyClient { self.inner_list(url)? .reverse(reverse.unwrap_or(false)) .limit(limit.unwrap_or(u16::MAX)) + .shallow(shallow.unwrap_or(false)) .send() .await .map(|urls| { From e163e7f22975d52dd7d341b8cae566cbd13075d5 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 8 Aug 2024 12:02:46 +0300 Subject: [PATCH 066/125] test(pubky): thorough testing for list options --- .../src/database/tables/entries.rs | 136 ++++++------ pubky/src/shared/public.rs | 201 ++++++++++++++++-- 2 files changed, 255 insertions(+), 82 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index af26828..7c61bf6 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -103,85 +103,93 @@ impl DB { let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); - // Remove directories from the cursor; - let cursor = cursor + // TODO: make this more performant than split and allocations? + let mut threshold = cursor .as_deref() - .and_then(|mut cursor| cursor.rsplit('/').next()) - .unwrap_or(if reverse { "~" } else { "" }); + .map(|mut cursor| { + // Get the name of the file or directory + // Similar to Path::new(cursor).file_name + let is_directory = cursor.ends_with('/'); - let mut cursor = format!("{path}{cursor}"); + let mut split = cursor.rsplit('/'); - // Fetch data based on direction - if reverse { - for _ in 0..limit { - if let Some((key, _)) = self.tables.entries.get_lower_than(txn, &cursor)? { - if !key.starts_with(path) { - break; - } + if is_directory { + // Move one step back + split.next(); + } - if shallow { - let mut split = key[path.len()..].split('/'); - let item = split.next().expect("should not be reachable"); + let file_or_directory = split.next().expect("should not be reachable"); - let is_directory = split.next().is_some(); + next_threshold(path, file_or_directory, is_directory, reverse, shallow) + }) + .unwrap_or(next_threshold(path, "", false, reverse, shallow)); - cursor = format!( - "{}{}", - &key[..(path.len() + item.len())], - // `.` is immediately lower than `/` - if is_directory { "." } else { "" } - ); + for _ in 0..limit { + if let Some((key, _)) = (if reverse { + self.tables.entries.get_lower_than(txn, &threshold)? + } else { + self.tables.entries.get_greater_than(txn, &threshold)? + }) { + if !key.starts_with(path) { + break; + } - let url = format!( - "pubky://{path}{item}{}", - if is_directory { "/" } else { "" } - ); + if shallow { + let mut split = key[path.len()..].split('/'); + let file_or_directory = split.next().expect("should not be reachable"); - results.push(url); - } else { - cursor = key.to_string(); - results.push(format!("pubky://{}", key)) - } - }; - } - } else { - for _ in 0..limit { - if let Some((key, _)) = self.tables.entries.get_greater_than(txn, &cursor)? { - if !key.starts_with(path) { - break; - } + let is_directory = split.next().is_some(); - if shallow { - let mut split = key[path.len()..].split('/'); - let item = split.next().expect("should not be reachable"); + threshold = + next_threshold(path, file_or_directory, is_directory, reverse, shallow); - let is_directory = split.next().is_some(); - - cursor = format!( - "{}{}", - &key[..(path.len() + item.len())], - // `0` is immediately higher than `/` - if is_directory { "0" } else { "" } - ); - - let url = format!( - "pubky://{path}{item}{}", - if is_directory { "/" } else { "" } - ); - - results.push(url); - } else { - cursor = key.to_string(); - results.push(format!("pubky://{}", key)) - } - }; - } - }; + results.push(format!( + "pubky://{path}{file_or_directory}{}", + if is_directory { "/" } else { "" } + )); + } else { + threshold = key.to_string(); + results.push(format!("pubky://{}", key)) + } + }; + } Ok(results) } } +/// Calculate the next threshold, only for flat (non-`shallow`) listing +fn next_threshold( + path: &str, + file_or_directory: &str, + is_directory: bool, + reverse: bool, + shallow: bool, +) -> String { + format!( + "{path}{file_or_directory}{}", + if file_or_directory.is_empty() { + // No file_or_directory, early return + if reverse { + // `path/to/dir/\x7f` to catch all paths than `path/to/dir/` + "\x7f" + } else { + "" + } + } else if shallow & is_directory { + if reverse { + // threshold = `path/to/dir\x2e`, since `\x2e` is lower than `/` + "\x2e" + } else { + //threshold = `path/to/dir\x7f`, since `\x7f` is greater than `/` + "\x7f" + } + } else { + "" + } + ) +} + #[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Entry { /// Encoding version diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 9bd099f..8e8f1c5 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -226,6 +226,10 @@ mod tests { format!("pubky://{}/pub/a.wrong/a.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!( + "pubky://{}/pub/example.com/cc-nested/z.txt", + keypair.public_key() + ), format!("pubky://{}/pub/example.wrong/a.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), @@ -237,9 +241,10 @@ mod tests { } let url = format!("pubky://{}/pub/example.com/extra", keypair.public_key()); + let url = url.as_str(); { - let list = client.list(url.as_str()).unwrap().send().await.unwrap(); + let list = client.list(url).unwrap().send().await.unwrap(); assert_eq!( list, @@ -247,6 +252,10 @@ mod tests { format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!( + "pubky://{}/pub/example.com/cc-nested/z.txt", + keypair.public_key() + ), format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), ], "normal list with no limit or cursor" @@ -254,13 +263,7 @@ mod tests { } { - let list = client - .list(url.as_str()) - .unwrap() - .limit(2) - .send() - .await - .unwrap(); + let list = client.list(url).unwrap().limit(2).send().await.unwrap(); assert_eq!( list, @@ -274,7 +277,7 @@ mod tests { { let list = client - .list(url.as_str()) + .list(url) .unwrap() .limit(2) .cursor("a.txt") @@ -288,13 +291,36 @@ mod tests { format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), ], - "normal list with limit and a suffix cursor" + "normal list with limit and a file cursor" ); } { let list = client - .list(url.as_str()) + .list(url) + .unwrap() + .limit(2) + .cursor("cc-nested/") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!( + "pubky://{}/pub/example.com/cc-nested/z.txt", + keypair.public_key() + ), + format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + ], + "normal list with limit and a directory cursor" + ); + } + + { + let list = client + .list(url) .unwrap() .limit(2) .cursor(&format!( @@ -317,7 +343,7 @@ mod tests { { let list = client - .list(url.as_str()) + .list(url) .unwrap() .reverse(true) .send() @@ -328,6 +354,10 @@ mod tests { list, vec![ format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!( + "pubky://{}/pub/example.com/cc-nested/z.txt", + keypair.public_key() + ), format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), @@ -338,7 +368,7 @@ mod tests { { let list = client - .list(url.as_str()) + .list(url) .unwrap() .reverse(true) .limit(2) @@ -350,7 +380,10 @@ mod tests { list, vec![ format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!( + "pubky://{}/pub/example.com/cc-nested/z.txt", + keypair.public_key() + ), ], "reverse list with limit but no cursor" ); @@ -358,7 +391,7 @@ mod tests { { let list = client - .list(url.as_str()) + .list(url) .unwrap() .reverse(true) .limit(2) @@ -370,8 +403,11 @@ mod tests { assert_eq!( list, vec![ + format!( + "pubky://{}/pub/example.com/cc-nested/z.txt", + keypair.public_key() + ), format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), ], "reverse list with limit and cursor" ); @@ -407,10 +443,11 @@ mod tests { } let url = format!("pubky://{}/pub/", keypair.public_key()); + let url = url.as_str(); { let list = client - .list(url.as_str()) + .list(url) .unwrap() .shallow(true) .send() @@ -434,10 +471,73 @@ mod tests { { let list = client - .list(url.as_str()) + .list(url) .unwrap() .shallow(true) + .limit(2) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/a.com/", keypair.public_key()), + format!("pubky://{}/pub/example.com/", keypair.public_key()), + ], + "normal list shallow with limit but no cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .limit(2) + .cursor("example.com/a.txt") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/", keypair.public_key()), + format!("pubky://{}/pub/example.con", keypair.public_key()), + ], + "normal list shallow with limit and a file cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .limit(3) + .cursor("example.com/") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{}/pub/example.con/", keypair.public_key()), + format!("pubky://{}/pub/file", keypair.public_key()), + ], + "normal list shallow with limit and a directory cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() .reverse(true) + .shallow(true) .send() .await .unwrap(); @@ -456,5 +556,70 @@ mod tests { "reverse list shallow" ); } + + { + let list = client + .list(url) + .unwrap() + .reverse(true) + .shallow(true) + .limit(2) + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/z.com/", keypair.public_key()), + format!("pubky://{}/pub/file2", keypair.public_key()), + ], + "reverse list shallow with limit but no cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .reverse(true) + .limit(2) + .cursor("file2") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/file", keypair.public_key()), + format!("pubky://{}/pub/example.con/", keypair.public_key()), + ], + "reverse list shallow with limit and a file cursor" + ); + } + + { + let list = client + .list(url) + .unwrap() + .shallow(true) + .reverse(true) + .limit(2) + .cursor("example.con/") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{}/pub/example.com/", keypair.public_key()), + ], + "reverse list shallow with limit and a directory cursor" + ); + } } } From 2db1e2e1913c2b13186fe2883f27e9b773d09e9b Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 8 Aug 2024 13:37:56 +0300 Subject: [PATCH 067/125] chore(js): publish 0.1.13 --- pubky/pkg/package.json | 2 +- pubky/pkg/test/public.js | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 4bce927..0469ca4 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.12", + "version": "0.1.13", "license": "MIT", "repository": { "type": "git", diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 4b301e7..6355ee4 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -293,6 +293,36 @@ test('list shallow', async (t) => { ); } + { + let list = await client.list(url, null, false, 3, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/a.com/`, + `pubky://${pubky}/pub/example.com/`, + `pubky://${pubky}/pub/example.con`, + ], + "normal list shallow with limit" + ); + } + + { + let list = await client.list(url, `example.com/`, false, null, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/example.con`, + `pubky://${pubky}/pub/example.con/`, + `pubky://${pubky}/pub/file`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/z.com/`, + ], + "normal list shallow with cursor" + ); + } + { let list = await client.list(url, null, true, null, true); @@ -310,4 +340,18 @@ test('list shallow', async (t) => { "normal list shallow" ); } + + { + let list = await client.list(url, null, true, 3, true); + + t.deepEqual( + list, + [ + `pubky://${pubky}/pub/z.com/`, + `pubky://${pubky}/pub/file2`, + `pubky://${pubky}/pub/file`, + ], + "normal list shallow with limit" + ); + } }) From 386e51e5b6222abf960c6ac6f0c119882f2e18ae Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 8 Aug 2024 13:38:25 +0300 Subject: [PATCH 068/125] fix(homeserver): cursor is a pubky url --- .../src/database/tables/entries.rs | 34 +++++++++++-------- pubky/src/shared/public.rs | 20 +++++++++++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 7c61bf6..6398dfa 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -2,6 +2,7 @@ use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, fmt::Result, time::SystemTime}; +use tracing::{debug, instrument}; use heed::{ types::{Bytes, Str}, @@ -104,23 +105,23 @@ impl DB { let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); // TODO: make this more performant than split and allocations? + let mut threshold = cursor - .as_deref() - .map(|mut cursor| { - // Get the name of the file or directory - // Similar to Path::new(cursor).file_name - let is_directory = cursor.ends_with('/'); + .map(|cursor| { + // Removing leading forward slashes + let mut file_or_directory = cursor.trim_start_matches('/'); - let mut split = cursor.rsplit('/'); + if cursor.starts_with("pubky://") { + file_or_directory = cursor.split(path).last().expect("should not be reachable") + }; - if is_directory { - // Move one step back - split.next(); - } - - let file_or_directory = split.next().expect("should not be reachable"); - - next_threshold(path, file_or_directory, is_directory, reverse, shallow) + next_threshold( + path, + file_or_directory, + file_or_directory.ends_with('/'), + reverse, + shallow, + ) }) .unwrap_or(next_threshold(path, "", false, reverse, shallow)); @@ -158,7 +159,8 @@ impl DB { } } -/// Calculate the next threshold, only for flat (non-`shallow`) listing +/// Calculate the next threshold +#[instrument] fn next_threshold( path: &str, file_or_directory: &str, @@ -166,6 +168,8 @@ fn next_threshold( reverse: bool, shallow: bool, ) -> String { + debug!("Fuck me!"); + format!( "{path}{file_or_directory}{}", if file_or_directory.is_empty() { diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 8e8f1c5..54ffd8e 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -341,6 +341,26 @@ mod tests { ); } + { + let list = client + .list(url) + .unwrap() + .limit(2) + .cursor("/a.txt") + .send() + .await + .unwrap(); + + assert_eq!( + list, + vec![ + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + ], + "normal list with limit and a leading / cursor" + ); + } + { let list = client .list(url) From 21c00793680a5baae5f86dbf458692e5f79f4f07 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 8 Aug 2024 13:57:04 +0300 Subject: [PATCH 069/125] docs(pubky): small docs improvements --- pubky/src/lib.rs | 3 +++ pubky/src/shared/auth.rs | 4 ++-- pubky/src/shared/list_builder.rs | 5 +++-- pubky/src/shared/public.rs | 8 ++++---- pubky/src/wasm.rs | 10 +++++++--- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index ab6732b..241e3ed 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -21,6 +21,9 @@ use ::pkarr::PkarrClientAsync; pub use error::Error; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::shared::list_builder::ListBuilder; + #[derive(Debug, Clone)] #[wasm_bindgen] pub struct PubkyClient { diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 3f900ce..0ee15a6 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -74,7 +74,7 @@ impl PubkyClient { } /// Signout from a homeserver. - pub async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { + pub(crate) async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { let Endpoint { public_key, mut url, @@ -90,7 +90,7 @@ impl PubkyClient { } /// Signin to a homeserver. - pub async fn inner_signin(&self, keypair: &Keypair) -> Result<()> { + pub(crate) async fn inner_signin(&self, keypair: &Keypair) -> Result<()> { let pubky = keypair.public_key(); let Endpoint { diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs index 8c5cde0..601f71d 100644 --- a/pubky/src/shared/list_builder.rs +++ b/pubky/src/shared/list_builder.rs @@ -15,7 +15,7 @@ pub struct ListBuilder<'a> { impl<'a> ListBuilder<'a> { /// Create a new List request builder - pub fn new(client: &'a PubkyClient, url: Url) -> Self { + pub(crate) fn new(client: &'a PubkyClient, url: Url) -> Self { Self { client, url, @@ -40,7 +40,8 @@ impl<'a> ListBuilder<'a> { /// Set the `cursor` value. /// - /// usually the last url from previous responses. + /// Either a full `pubky://` Url (from previous list response), + /// or a path (to a file or directory) relative to the `url` pub fn cursor(mut self, cursor: &'a str) -> Self { self.cursor = cursor.into(); self diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 54ffd8e..82bfbd8 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -12,7 +12,7 @@ use crate::{ use super::{list_builder::ListBuilder, pkarr::Endpoint}; impl PubkyClient { - pub async fn inner_put>(&self, url: T, content: &[u8]) -> Result<()> { + pub(crate) async fn inner_put>(&self, url: T, content: &[u8]) -> Result<()> { let url = self.pubky_to_http(url).await?; let response = self @@ -26,7 +26,7 @@ impl PubkyClient { Ok(()) } - pub async fn inner_get>(&self, url: T) -> Result> { + pub(crate) async fn inner_get>(&self, url: T) -> Result> { let url = self.pubky_to_http(url).await?; let response = self.request(Method::GET, url).send().await?; @@ -43,7 +43,7 @@ impl PubkyClient { Ok(Some(bytes)) } - pub async fn inner_delete>(&self, url: T) -> Result<()> { + pub(crate) async fn inner_delete>(&self, url: T) -> Result<()> { let url = self.pubky_to_http(url).await?; let response = self.request(Method::DELETE, url).send().await?; @@ -53,7 +53,7 @@ impl PubkyClient { Ok(()) } - pub fn inner_list>(&self, url: T) -> Result { + pub(crate) fn inner_list>(&self, url: T) -> Result { Ok(ListBuilder::new( self, url.try_into().map_err(|_| Error::InvalidUrl)?, diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 07b6749..536949f 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -154,10 +154,14 @@ impl PubkyClient { self.inner_delete(url).await.map_err(|e| e.into()) } - /// Returns a list of Pubky URLs of the files within the `url` path, - /// respecting the `cursor`, `reverse` and `limit` options. + /// Returns a list of Pubky urls (as strings). /// - /// `cursor` is usually the last url from previous responses. + /// - `url`: The Pubky url (string) to the directory you want to list its content. + /// - `cursor`: Either a full `pubky://` Url (from previous list response), + /// or a path (to a file or directory) relative to the `url` + /// - `reverse`: List in reverse order + /// - `limit` Limit the number of urls in the response + /// - `shallow`: List directories and files, instead of flat list of files. #[wasm_bindgen] pub async fn list( &self, From e82e44cc2ded4ad379d4818eb81260238ea6f832 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 8 Aug 2024 14:25:36 +0300 Subject: [PATCH 070/125] fix(js): return better error for Keypair.fromSecretKey() with invalid input --- pubky/pkg/test/keys.js | 8 ++++++++ pubky/src/wasm/keys.rs | 19 ++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index e184b3b..d036862 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -11,3 +11,11 @@ test('generate keys from a seed', async (t) => { t.is(publicKey.z32(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') }) + +test('fromSecretKey error', async (t) => { + const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b', 'hex') + + + t.throws(() => Keypair.fromSecretKey(null), /Expected secret_key to be an instance of Uint8Array/) + t.throws(() => Keypair.fromSecretKey(secretkey), /Expected secret_key to be 32 bytes, got 15/) +}) diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 345e721..12ecdd7 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -15,11 +15,20 @@ impl Keypair { /// Generate a [Keypair] from a secret key. #[wasm_bindgen(js_name = "fromSecretKey")] - pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { + pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Result { + if !js_sys::Uint8Array::instanceof(&secret_key) { + return Err("Expected secret_key to be an instance of Uint8Array".into()); + } + + let len = secret_key.byte_length(); + if (len != 32) { + return Err(format!("Expected secret_key to be 32 bytes, got {len}"))?; + } + let mut bytes = [0; 32]; secret_key.copy_to(&mut bytes); - Self(pkarr::Keypair::from_secret_key(&bytes)) + Ok(Self(pkarr::Keypair::from_secret_key(&bytes))) } /// Returns the secret key of this keypair. @@ -67,9 +76,9 @@ impl PublicKey { #[wasm_bindgen(js_name = "from")] /// @throws pub fn try_from(value: JsValue) -> Result { - let string = value.as_string().ok_or(Error::Generic( - "Couldn't create a PublicKey from this type of value".to_string(), - ))?; + let string = value + .as_string() + .ok_or("Couldn't create a PublicKey from this type of value")?; Ok(PublicKey( pkarr::PublicKey::try_from(string).map_err(Error::Pkarr)?, From 7c6d961e91f9c55ce8059a020ded85fed33072ac Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 8 Aug 2024 14:46:11 +0300 Subject: [PATCH 071/125] chore(js): publish 0.1.14 --- pubky/pkg/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 0469ca4..3dd75ba 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.13", + "version": "0.1.14", "license": "MIT", "repository": { "type": "git", From c81e418cb31e33973821432b27abe065be3e8e7b Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 20 Aug 2024 10:16:21 +0300 Subject: [PATCH 072/125] chore(homeserver): remove unused --- pubky-homeserver/src/config.rs | 2 +- pubky-homeserver/src/database.rs | 23 ++++++-------- pubky-homeserver/src/database/migrations.rs | 4 +-- .../src/database/migrations/m0.rs | 2 +- pubky-homeserver/src/database/tables/blobs.rs | 9 ++---- .../src/database/tables/entries.rs | 24 ++++---------- .../src/database/tables/sessions.rs | 9 ++---- pubky-homeserver/src/database/tables/users.rs | 6 ++-- pubky-homeserver/src/error.rs | 31 ++++++++++++------- pubky-homeserver/src/extractors.rs | 4 --- pubky-homeserver/src/lib.rs | 2 -- pubky-homeserver/src/routes.rs | 8 +---- pubky-homeserver/src/routes/auth.rs | 14 ++------- pubky-homeserver/src/routes/pkarr.rs | 7 ++--- pubky-homeserver/src/routes/public.rs | 29 +++++------------ pubky-homeserver/src/server.rs | 8 ++--- 16 files changed, 61 insertions(+), 121 deletions(-) diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 22686cf..8fea08b 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -1,7 +1,7 @@ //! Configuration for the server use anyhow::{anyhow, Result}; -use pkarr::{mainline::dht::DhtSettings, Keypair}; +use pkarr::Keypair; // use serde::{Deserialize, Serialize}; use std::{fmt::Debug, path::PathBuf, time::Duration}; diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index bbd21b8..f7174ee 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -2,18 +2,12 @@ use std::fs; use std::path::Path; -use bytes::Bytes; -use heed::{types::Str, Database, Env, EnvOpenOptions, RwTxn}; +use heed::{Env, EnvOpenOptions}; mod migrations; pub mod tables; -use pubky_common::crypto::Hasher; - -use tables::{entries::Entry, Tables, TABLES_COUNT}; - -use pkarr::PublicKey; -use tables::blobs::{BlobsTable, BLOBS_TABLE}; +use tables::{Tables, TABLES_COUNT}; #[derive(Debug, Clone)] pub struct DB { @@ -37,12 +31,11 @@ impl DB { #[cfg(test)] mod tests { + use bytes::Bytes; use pkarr::Keypair; use pubky_common::timestamp::Timestamp; - use crate::config::Config; - - use super::{Bytes, DB}; + use super::DB; #[tokio::test] async fn entries() { @@ -61,13 +54,15 @@ mod tests { let cloned_keypair = keypair.clone(); let done = tokio::task::spawn_blocking(move || { - cloned.put_entry(&cloned_keypair.public_key(), path, rx); + cloned + .put_entry(&cloned_keypair.public_key(), path, rx) + .unwrap(); }); - tx.send(vec![1, 2, 3, 4, 5].into()); + tx.send(vec![1, 2, 3, 4, 5].into()).unwrap(); drop(tx); - done.await; + done.await.unwrap(); let blob = db.get_blob(&keypair.public_key(), path).unwrap().unwrap(); diff --git a/pubky-homeserver/src/database/migrations.rs b/pubky-homeserver/src/database/migrations.rs index 32f2909..eb5a5f8 100644 --- a/pubky-homeserver/src/database/migrations.rs +++ b/pubky-homeserver/src/database/migrations.rs @@ -1,4 +1,4 @@ -use heed::{types::Str, Database, Env, RwTxn}; +use heed::Env; mod m0; @@ -7,7 +7,7 @@ use super::tables::Tables; pub fn run(env: &Env) -> anyhow::Result { let mut wtxn = env.write_txn()?; - m0::run(env, &mut wtxn); + m0::run(env, &mut wtxn)?; let tables = Tables::new(env, &mut wtxn)?; diff --git a/pubky-homeserver/src/database/migrations/m0.rs b/pubky-homeserver/src/database/migrations/m0.rs index d7dac79..a690049 100644 --- a/pubky-homeserver/src/database/migrations/m0.rs +++ b/pubky-homeserver/src/database/migrations/m0.rs @@ -1,4 +1,4 @@ -use heed::{types::Str, Database, Env, RwTxn}; +use heed::{Env, RwTxn}; use crate::database::tables::{blobs, entries, sessions, users}; diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs index f65dbe9..1a02a09 100644 --- a/pubky-homeserver/src/database/tables/blobs.rs +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -1,9 +1,4 @@ -use std::{borrow::Cow, time::SystemTime}; - -use heed::{ - types::{Bytes, Str}, - BoxedError, BytesDecode, BytesEncode, Database, -}; +use heed::{types::Bytes, Database}; use pkarr::PublicKey; use crate::database::DB; @@ -36,7 +31,7 @@ impl DB { None }; - rtxn.commit(); + rtxn.commit()?; Ok(result) } diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 6398dfa..22a3aa4 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -1,12 +1,11 @@ use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, fmt::Result, time::SystemTime}; use tracing::{debug, instrument}; use heed::{ types::{Bytes, Str}, - BoxedError, BytesDecode, BytesEncode, Database, RoTxn, + Database, RoTxn, }; use pubky_common::{ @@ -53,7 +52,9 @@ impl DB { let key = format!("{public_key}/{path}"); - self.tables.entries.put(&mut wtxn, &key, &entry.serialize()); + self.tables + .entries + .put(&mut wtxn, &key, &entry.serialize())?; wtxn.commit()?; @@ -126,11 +127,11 @@ impl DB { .unwrap_or(next_threshold(path, "", false, reverse, shallow)); for _ in 0..limit { - if let Some((key, _)) = (if reverse { + if let Some((key, _)) = if reverse { self.tables.entries.get_lower_than(txn, &threshold)? } else { self.tables.entries.get_greater_than(txn, &threshold)? - }) { + } { if !key.starts_with(path) { break; } @@ -228,25 +229,12 @@ impl Entry { self } - pub fn set_content_type(&mut self, content_type: &str) -> &mut Self { - self.content_type = content_type.to_string(); - self - } - // === Getters === pub fn content_hash(&self) -> &[u8; 32] { &self.content_hash } - pub fn content_length(&self) -> usize { - self.content_length - } - - pub fn content_type(&self) -> &str { - &self.content_type - } - // === Public Method === pub fn serialize(&self) -> Vec { diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs index db6c53d..4ecd228 100644 --- a/pubky-homeserver/src/database/tables/sessions.rs +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -1,12 +1,9 @@ -use std::{borrow::Cow, time::SystemTime}; - use heed::{ types::{Bytes, Str}, - BoxedError, BytesDecode, BytesEncode, Database, + Database, }; use pkarr::PublicKey; use pubky_common::session::Session; -use serde::Deserialize; use tower_cookies::Cookies; use crate::database::DB; @@ -21,9 +18,8 @@ impl DB { &mut self, cookies: Cookies, public_key: &PublicKey, - path: &str, ) -> anyhow::Result> { - if let Some(bytes) = self.get_session_bytes(cookies, public_key, path)? { + if let Some(bytes) = self.get_session_bytes(cookies, public_key)? { return Ok(Some(Session::deserialize(&bytes)?)); }; @@ -34,7 +30,6 @@ impl DB { &mut self, cookies: Cookies, public_key: &PublicKey, - path: &str, ) -> anyhow::Result>> { if let Some(cookie) = cookies.get(&public_key.to_string()) { let rtxn = self.env.read_txn()?; diff --git a/pubky-homeserver/src/database/tables/users.rs b/pubky-homeserver/src/database/tables/users.rs index 9666637..cf9b44e 100644 --- a/pubky-homeserver/src/database/tables/users.rs +++ b/pubky-homeserver/src/database/tables/users.rs @@ -1,14 +1,12 @@ -use std::{borrow::Cow, time::SystemTime}; +use std::borrow::Cow; use postcard::{from_bytes, to_allocvec}; -use pubky_common::timestamp::Timestamp; use serde::{Deserialize, Serialize}; -use heed::{types::Str, BoxedError, BytesDecode, BytesEncode, Database}; +use heed::{BoxedError, BytesDecode, BytesEncode, Database}; use pkarr::PublicKey; extern crate alloc; -use alloc::vec::Vec; /// PublicKey => User. pub type UsersTable = Database; diff --git a/pubky-homeserver/src/error.rs b/pubky-homeserver/src/error.rs index 57081ab..5fad2f0 100644 --- a/pubky-homeserver/src/error.rs +++ b/pubky-homeserver/src/error.rs @@ -6,7 +6,6 @@ use axum::{ response::IntoResponse, }; use pubky_common::auth::AuthnSignatureError; -use tracing::debug; pub type Result = core::result::Result; @@ -70,6 +69,22 @@ impl From for Error { } } +// === Pubky specific errors === + +impl From for Error { + fn from(error: AuthnSignatureError) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) + } +} + +impl From for Error { + fn from(error: pkarr::Error) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) + } +} + +// === INTERNAL_SERVER_ERROR === + impl From for Error { fn from(error: std::io::Error) -> Self { Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) @@ -100,16 +115,8 @@ impl From for Error { } } -// === Pubky specific errors === - -impl From for Error { - fn from(error: AuthnSignatureError) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) - } -} - -impl From for Error { - fn from(error: pkarr::Error) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(error)) +impl From> for Error { + fn from(error: flume::SendError) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) } } diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs index b89911c..567ca6b 100644 --- a/pubky-homeserver/src/extractors.rs +++ b/pubky-homeserver/src/extractors.rs @@ -52,10 +52,6 @@ impl EntryPath { pub fn as_str(&self) -> &str { self.0.as_str() } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } } #[async_trait] diff --git a/pubky-homeserver/src/lib.rs b/pubky-homeserver/src/lib.rs index 51852ec..4a1253b 100644 --- a/pubky-homeserver/src/lib.rs +++ b/pubky-homeserver/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(unused)] - pub mod config; mod database; mod error; diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 4d057a3..35615fd 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -1,16 +1,10 @@ -use std::sync::Arc; - use axum::{ extract::DefaultBodyLimit, - http::Method, routing::{delete, get, post, put}, Router, }; use tower_cookies::CookieManagerLayer; -use tower_http::{ - cors::{self, CorsLayer}, - trace::TraceLayer, -}; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; use crate::server::AppState; diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 7feafbb..72246f9 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -1,21 +1,14 @@ use axum::{ debug_handler, - extract::{Request, State}, - http::{uri::Scheme, HeaderMap, StatusCode, Uri}, + extract::State, + http::{uri::Scheme, StatusCode, Uri}, response::IntoResponse, - Router, }; use axum_extra::{headers::UserAgent, TypedHeader}; use bytes::Bytes; -use heed::BytesEncode; -use postcard::to_allocvec; use tower_cookies::{cookie::SameSite, Cookie, Cookies}; -use pubky_common::{ - crypto::{random_bytes, random_hash}, - session::Session, - timestamp::Timestamp, -}; +use pubky_common::{crypto::random_bytes, session::Session, timestamp::Timestamp}; use crate::{ database::tables::{ @@ -43,7 +36,6 @@ pub async fn signup( pub async fn session( State(state): State, - TypedHeader(user_agent): TypedHeader, cookies: Cookies, pubky: Pubky, ) -> Result { diff --git a/pubky-homeserver/src/routes/pkarr.rs b/pubky-homeserver/src/routes/pkarr.rs index 977a129..9e40230 100644 --- a/pubky-homeserver/src/routes/pkarr.rs +++ b/pubky-homeserver/src/routes/pkarr.rs @@ -1,5 +1,3 @@ -use std::{collections::HashMap, sync::RwLock}; - use axum::{ body::{Body, Bytes}, extract::State, @@ -10,8 +8,7 @@ use axum::{ }; use futures_util::stream::StreamExt; -use pkarr::{PublicKey, SignedPacket}; -use tracing::debug; +use pkarr::SignedPacket; use crate::{ error::{Error, Result}, @@ -31,7 +28,7 @@ pub fn pkarr_router(state: AppState) -> Router { } pub async fn pkarr_put( - State(mut state): State, + State(state): State, pubky: Pubky, body: Body, ) -> Result { diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 33a6da3..4dc1bf6 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -2,27 +2,15 @@ use std::collections::HashMap; use axum::{ body::{Body, Bytes}, - debug_handler, - extract::{Path, Query, State}, + extract::{Query, State}, http::{header, Response, StatusCode}, response::IntoResponse, - RequestExt, Router, }; -use axum_extra::body::AsyncReadBody; use futures_util::stream::StreamExt; use pkarr::PublicKey; -use serde::Deserialize; use tower_cookies::Cookies; -use tracing::debug; - -use pubky_common::crypto::Hasher; - use crate::{ - database::tables::{ - blobs::{BlobsTable, BLOBS_TABLE}, - entries::{EntriesTable, Entry, ENTRIES_TABLE}, - }, error::{Error, Result}, extractors::{EntryPath, Pubky}, server::AppState, @@ -33,7 +21,7 @@ pub async fn put( pubky: Pubky, path: EntryPath, cookies: Cookies, - mut body: Body, + body: Body, ) -> Result { let public_key = pubky.public_key().clone(); let path = path.as_str(); @@ -54,7 +42,7 @@ pub async fn put( // to stream this to filesystem, and keep track of any failed // writes to GC these files later. - state.db.put_entry(&public_key, &path, rx); + state.db.put_entry(&public_key, &path, rx)?; Ok(()) }); @@ -62,7 +50,7 @@ pub async fn put( while let Some(next) = stream.next().await { let chunk = next?; - tx.send(chunk); + tx.send(chunk)?; } drop(tx); @@ -79,7 +67,7 @@ pub async fn get( path: EntryPath, Query(params): Query>, ) -> Result { - verify(path.as_str()); + verify(path.as_str())?; let public_key = pubky.public_key(); let path = path.as_str(); @@ -127,7 +115,6 @@ pub async fn delete( pubky: Pubky, path: EntryPath, cookies: Cookies, - mut body: Body, ) -> Result { let public_key = pubky.public_key().clone(); let path = path.as_str(); @@ -151,13 +138,13 @@ fn authorize( state: &mut AppState, cookies: Cookies, public_key: &PublicKey, - path: &str, + _: &str, ) -> Result<()> { // TODO: can we move this logic to the extractor or a layer // to perform this validation? - let session = state + let _ = state .db - .get_session(cookies, public_key, path)? + .get_session(cookies, public_key)? .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; Ok(()) diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 2863317..3db7441 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,15 +1,13 @@ -use std::{ - collections::HashMap, future::IntoFuture, net::SocketAddr, num::NonZeroUsize, sync::Arc, -}; +use std::{future::IntoFuture, net::SocketAddr}; use anyhow::{Error, Result}; use pubky_common::auth::AuthnVerifier; -use tokio::{net::TcpListener, signal, sync::Mutex, task::JoinSet}; +use tokio::{net::TcpListener, signal, task::JoinSet}; use tracing::{debug, info, warn}; use pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PkarrClientAsync, PublicKey, Settings, SignedPacket, + PkarrClient, PkarrClientAsync, PublicKey, Settings, }; use crate::{config::Config, database::DB, pkarr::publish_server_packet}; From 4391b1c7ffbd71976e496ebbbe70989344264a27 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 20 Aug 2024 10:22:05 +0300 Subject: [PATCH 073/125] chore(pubky): remove unused --- pubky/src/lib.rs | 2 -- pubky/src/native.rs | 6 +++--- pubky/src/shared/auth.rs | 14 ++------------ pubky/src/shared/list_builder.rs | 2 +- pubky/src/shared/pkarr.rs | 8 ++++---- pubky/src/shared/public.rs | 23 ++++------------------- pubky/src/shared/recovery_file.rs | 7 +++---- 7 files changed, 17 insertions(+), 45 deletions(-) diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 241e3ed..2b6cf42 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(unused)] - mod error; mod shared; diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 6f9f676..0ca2d7c 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -7,7 +7,7 @@ use ::pkarr::{ use bytes::Bytes; use pkarr::Keypair; use pubky_common::session::Session; -use reqwest::{Method, RequestBuilder, Response}; +use reqwest::{RequestBuilder, Response}; use url::Url; use crate::{ @@ -150,6 +150,6 @@ impl PubkyClient { self.http.request(method, url) } - pub(crate) fn store_session(&self, response: Response) {} - pub(crate) fn remove_session(&self, pubky: &PublicKey) {} + pub(crate) fn store_session(&self, _: Response) {} + pub(crate) fn remove_session(&self, _: &PublicKey) {} } diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 0ee15a6..d4a6436 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -2,12 +2,8 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; -use url::Url; -use crate::{ - error::{Error, Result}, - PubkyClient, -}; +use crate::{error::Result, PubkyClient}; use super::pkarr::Endpoint; @@ -75,10 +71,7 @@ impl PubkyClient { /// Signout from a homeserver. pub(crate) async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { - let Endpoint { - public_key, - mut url, - } = self.resolve_pubky_homeserver(pubky).await?; + let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); @@ -115,14 +108,11 @@ impl PubkyClient { #[cfg(test)] mod tests { - use std::time::Duration; - use crate::*; use pkarr::{mainline::Testnet, Keypair}; use pubky_common::session::Session; use pubky_homeserver::Homeserver; - use tokio::time::sleep; #[tokio::test] async fn basic_authn() { diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs index 601f71d..0eaec77 100644 --- a/pubky/src/shared/list_builder.rs +++ b/pubky/src/shared/list_builder.rs @@ -1,4 +1,4 @@ -use reqwest::{Method, Response, StatusCode}; +use reqwest::Method; use url::Url; use crate::{error::Result, PubkyClient}; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index f615de0..01cd0fb 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -1,4 +1,4 @@ -use url::{Origin, Url}; +use url::Url; use pkarr::{ dns::{rdata::SVCB, Packet}, @@ -128,7 +128,7 @@ impl PubkyClient { } if let Some(public_key) = homeserver_public_key { - let mut url = Url::parse(&format!( + let url = Url::parse(&format!( "{}://{}", if origin.starts_with("localhost") { "http" @@ -156,7 +156,7 @@ mod tests { use pkarr::{ dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Dht, Testnet}, + mainline::{dht::DhtSettings, Testnet}, Keypair, PkarrClient, Settings, SignedPacket, }; use pubky_homeserver::Homeserver; @@ -183,7 +183,7 @@ mod tests { let server_tld = server.public_key().to_string(); - let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + let svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); packet.answers.push(pkarr::dns::ResourceRecord::new( "pubky".try_into().unwrap(), diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 82bfbd8..7dbb18e 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use pkarr::PublicKey; -use reqwest::{Method, Response, StatusCode}; +use reqwest::{Method, StatusCode}; use url::Url; use crate::{ @@ -61,7 +61,7 @@ impl PubkyClient { } pub(crate) async fn pubky_to_http>(&self, url: T) -> Result { - let mut original_url: Url = url.try_into().map_err(|_| Error::InvalidUrl)?; + let original_url: Url = url.try_into().map_err(|_| Error::InvalidUrl)?; if original_url.scheme() != "pubky" { return Ok(original_url); @@ -92,21 +92,6 @@ impl PubkyClient { } } -fn normalize_path(path: &str) -> Result { - let mut path = path.to_string(); - - if path.starts_with('/') { - path = path[1..].to_string() - } - - // TODO: should we return error instead? - if path.ends_with('/') { - path = path[..path.len()].to_string() - } - - Ok(path) -} - #[cfg(test)] mod tests { @@ -177,7 +162,7 @@ mod tests { Err(Error::Reqwest(error)) => { assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) } - error => { + _ => { panic!("expected error StatusCode::UNAUTHORIZED") } } @@ -200,7 +185,7 @@ mod tests { Err(Error::Reqwest(error)) => { assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) } - error => { + _ => { panic!("expected error StatusCode::UNAUTHORIZED") } } diff --git a/pubky/src/shared/recovery_file.rs b/pubky/src/shared/recovery_file.rs index 5f500ff..9fff885 100644 --- a/pubky/src/shared/recovery_file.rs +++ b/pubky/src/shared/recovery_file.rs @@ -2,10 +2,7 @@ use argon2::Argon2; use pkarr::Keypair; use pubky_common::crypto::{decrypt, encrypt}; -use crate::{ - error::{Error, Result}, - PubkyClient, -}; +use crate::error::{Error, Result}; static SPEC_NAME: &str = "recovery"; static SPEC_LINE: &str = "pubky.org/recovery"; @@ -68,6 +65,8 @@ fn recovery_file_encryption_key_from_passphrase(passphrase: &str) -> Result<[u8; mod tests { use super::*; + use crate::PubkyClient; + #[test] fn encrypt_decrypt_recovery_file() { let passphrase = "very secure password"; From 0ce4a9da65129147e0c3cb7cb4507b97d5e26f62 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 20 Aug 2024 15:16:26 +0300 Subject: [PATCH 074/125] feat(common): impl serialize and deserialize for Timestamp --- pubky-common/src/timestamp.rs | 27 +++++++++++++++++++ .../src/database/tables/entries.rs | 7 ++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index 4c546d5..174c7e3 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -1,5 +1,6 @@ //! Monotonic unix timestamp in microseconds +use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::{ ops::{Add, Sub}, @@ -83,6 +84,12 @@ impl Timestamp { } } +impl Default for Timestamp { + fn default() -> Self { + Timestamp::now() + } +} + impl Display for Timestamp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let bytes: [u8; 8] = self.into(); @@ -155,6 +162,26 @@ impl Sub for &Timestamp { } } +impl Serialize for Timestamp { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bytes = self.to_bytes(); + bytes.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes: [u8; 8] = Deserialize::deserialize(deserializer)?; + Ok(Timestamp(u64::from_be_bytes(bytes))) + } +} + #[cfg(not(target_arch = "wasm32"))] /// Return the number of microseconds since [SystemTime::UNIX_EPOCH] fn system_time() -> u64 { diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 22a3aa4..70dafe4 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -200,7 +200,7 @@ pub struct Entry { /// Encoding version version: usize, /// Modified at - timestamp: u64, + timestamp: Timestamp, content_hash: [u8; 32], content_length: usize, content_type: String, @@ -211,10 +211,7 @@ pub struct Entry { impl Entry { pub fn new() -> Self { - Self { - timestamp: Timestamp::now().into_inner(), - ..Default::default() - } + Default::default() } // === Setters === From b4c7fdad4545211bb0e6d4b8780a7a2ffc2f947f Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 20 Aug 2024 21:53:01 +0300 Subject: [PATCH 075/125] feat(homeserver): add /events/ endpoint to list PUT/DELETE events --- pubky-homeserver/src/database.rs | 4 +- .../src/database/migrations/m0.rs | 4 +- pubky-homeserver/src/database/tables.rs | 9 ++- pubky-homeserver/src/database/tables/blobs.rs | 2 +- .../src/database/tables/entries.rs | 19 +++++- .../src/database/tables/events.rs | 54 +++++++++++++++ pubky-homeserver/src/routes.rs | 2 + pubky-homeserver/src/routes/feed.rs | 65 +++++++++++++++++++ pubky-homeserver/src/routes/public.rs | 4 +- 9 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 pubky-homeserver/src/database/tables/events.rs create mode 100644 pubky-homeserver/src/routes/feed.rs diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index f7174ee..4adc73d 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -9,6 +9,8 @@ pub mod tables; use tables::{Tables, TABLES_COUNT}; +pub const MAX_LIST_LIMIT: u16 = 100; + #[derive(Debug, Clone)] pub struct DB { pub(crate) env: Env, @@ -43,7 +45,7 @@ mod tests { .join(Timestamp::now().to_string()) .join("pubky"); - let mut db = DB::open(&storage).unwrap(); + let db = DB::open(&storage).unwrap(); let keypair = Keypair::random(); let path = "/pub/foo.txt"; diff --git a/pubky-homeserver/src/database/migrations/m0.rs b/pubky-homeserver/src/database/migrations/m0.rs index a690049..11c0e1a 100644 --- a/pubky-homeserver/src/database/migrations/m0.rs +++ b/pubky-homeserver/src/database/migrations/m0.rs @@ -1,6 +1,6 @@ use heed::{Env, RwTxn}; -use crate::database::tables::{blobs, entries, sessions, users}; +use crate::database::tables::{blobs, entries, events, sessions, users}; pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { let _: users::UsersTable = env.create_database(wtxn, Some(users::USERS_TABLE))?; @@ -11,5 +11,7 @@ pub fn run(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { let _: entries::EntriesTable = env.create_database(wtxn, Some(entries::ENTRIES_TABLE))?; + let _: events::EventsTable = env.create_database(wtxn, Some(events::EVENTS_TABLE))?; + Ok(()) } diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index a019fbe..81a87da 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -1,5 +1,6 @@ pub mod blobs; pub mod entries; +pub mod events; pub mod sessions; pub mod users; @@ -8,12 +9,15 @@ use heed::{Env, RwTxn}; use blobs::{BlobsTable, BLOBS_TABLE}; use entries::{EntriesTable, ENTRIES_TABLE}; -pub const TABLES_COUNT: u32 = 4; +use self::events::{EventsTable, EVENTS_TABLE}; + +pub const TABLES_COUNT: u32 = 5; #[derive(Debug, Clone)] pub struct Tables { pub blobs: BlobsTable, pub entries: EntriesTable, + pub events: EventsTable, } impl Tables { @@ -25,6 +29,9 @@ impl Tables { entries: env .open_database(wtxn, Some(ENTRIES_TABLE))? .expect("Entries table already created"), + events: env + .open_database(wtxn, Some(EVENTS_TABLE))? + .expect("Events table already created"), }) } } diff --git a/pubky-homeserver/src/database/tables/blobs.rs b/pubky-homeserver/src/database/tables/blobs.rs index 1a02a09..25f57c0 100644 --- a/pubky-homeserver/src/database/tables/blobs.rs +++ b/pubky-homeserver/src/database/tables/blobs.rs @@ -12,7 +12,7 @@ pub const BLOBS_TABLE: &str = "blobs"; impl DB { pub fn get_blob( - &mut self, + &self, public_key: &PublicKey, path: &str, ) -> anyhow::Result> { diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 70dafe4..d88b116 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -13,15 +13,15 @@ use pubky_common::{ timestamp::Timestamp, }; -use crate::database::DB; +use crate::database::{DB, MAX_LIST_LIMIT}; + +use super::events::Event; /// full_path(pubky/*path) => Entry. pub type EntriesTable = Database; pub const ENTRIES_TABLE: &str = "entries"; -const MAX_LIST_LIMIT: u16 = 100; - impl DB { pub fn put_entry( &mut self, @@ -56,6 +56,19 @@ impl DB { .entries .put(&mut wtxn, &key, &entry.serialize())?; + if path.starts_with("pub/") { + let url = format!("pubky://{key}"); + let event = Event::put(&url); + let value = event.serialize(); + + let key = entry.timestamp.to_string(); + + self.tables.events.put(&mut wtxn, &key, &value)?; + + // TODO: delete older events. + // TODO: move to events.rs + } + wtxn.commit()?; Ok(()) diff --git a/pubky-homeserver/src/database/tables/events.rs b/pubky-homeserver/src/database/tables/events.rs new file mode 100644 index 0000000..18173dc --- /dev/null +++ b/pubky-homeserver/src/database/tables/events.rs @@ -0,0 +1,54 @@ +//! Server events (Put and Delete entries) +//! +//! Useful as a realtime sync with Indexers until +//! we implement more self-authenticated merkle data. + +use heed::{ + types::{Bytes, Str}, + Database, +}; +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; + +/// Event [Timestamp] base32 => Encoded event. +pub type EventsTable = Database; + +pub const EVENTS_TABLE: &str = "events"; + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum Event { + Put(String), + Delete(String), +} + +impl Event { + pub fn put(url: &str) -> Self { + Self::Put(url.to_string()) + } + + pub fn serialize(&self) -> Vec { + to_allocvec(self).expect("Session::serialize") + } + + pub fn deserialize(bytes: &[u8]) -> core::result::Result { + if bytes[0] > 1 { + panic!("Unknown Event version"); + } + + from_bytes(bytes) + } + + pub fn url(&self) -> &str { + match self { + Event::Put(url) => url, + Event::Delete(url) => url, + } + } + + pub fn operation(&self) -> &str { + match self { + Event::Put(_) => "PUT", + Event::Delete(_) => "DEL", + } + } +} diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 35615fd..163baec 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -11,6 +11,7 @@ use crate::server::AppState; use self::pkarr::pkarr_router; mod auth; +mod feed; mod pkarr; mod public; mod root; @@ -25,6 +26,7 @@ fn base(state: AppState) -> Router { .route("/:pubky/*path", put(public::put)) .route("/:pubky/*path", get(public::get)) .route("/:pubky/*path", delete(public::delete)) + .route("/events/", get(feed::feed)) .layer(CookieManagerLayer::new()) // TODO: revisit if we enable streaming big payloads // TODO: maybe add to a separate router (drive router?). diff --git a/pubky-homeserver/src/routes/feed.rs b/pubky-homeserver/src/routes/feed.rs new file mode 100644 index 0000000..734d239 --- /dev/null +++ b/pubky-homeserver/src/routes/feed.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +use axum::{ + body::Body, + extract::{Query, State}, + http::{header, Response, StatusCode}, + response::IntoResponse, +}; + +use crate::{ + database::{tables::events::Event, MAX_LIST_LIMIT}, + error::Result, + server::AppState, +}; + +pub async fn feed( + State(state): State, + Query(params): Query>, +) -> Result { + let txn = state.db.env.read_txn()?; + + let limit = params + .get("limit") + .and_then(|l| l.parse::().ok()) + .unwrap_or(MAX_LIST_LIMIT) + .min(MAX_LIST_LIMIT); + + let mut cursor = params + .get("cursor") + .map(|c| c.as_str()) + .unwrap_or("0000000000000"); + + if cursor.len() < 13 { + cursor = "0000000000000" + } + + let mut result: Vec = vec![]; + let mut next_cursor = "".to_string(); + + for _ in 0..limit { + match state.db.tables.events.get_greater_than(&txn, cursor)? { + Some((timestamp, event_bytes)) => { + let event = Event::deserialize(event_bytes)?; + + let line = format!("{} {}", event.operation(), event.url()); + next_cursor = timestamp.to_string(); + + result.push(line); + } + None => break, + }; + } + + if !result.is_empty() { + result.push(format!("cursor: {next_cursor}")) + } + + txn.commit()?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "text/plain") + .body(Body::from(result.join("\n"))) + .unwrap()) +} diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 4dc1bf6..cdfb0a9 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -62,7 +62,7 @@ pub async fn put( } pub async fn get( - State(mut state): State, + State(state): State, pubky: Pubky, path: EntryPath, Query(params): Query>, @@ -96,7 +96,7 @@ pub async fn get( return Ok(Response::builder() .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_TYPE, "text/plain") .body(Body::from(vec.join("\n"))) .unwrap()); } From 51e26257530c6bb10e967a320b87fd13bf9a6eca Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 20 Aug 2024 21:57:13 +0300 Subject: [PATCH 076/125] feat(homeserver): add delete events when deleting and item --- pubky-homeserver/src/database/tables/entries.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index d88b116..e515941 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -87,6 +87,21 @@ impl DB { let deleted_entry = self.tables.entries.delete(&mut wtxn, &key)?; + // create DELETE event + if path.starts_with("pub/") { + let url = format!("pubky://{key}"); + + let event = Event::put(&url); + let value = event.serialize(); + + let key = entry.timestamp.to_string(); + + self.tables.events.put(&mut wtxn, &key, &value)?; + + // TODO: delete older events. + // TODO: move to events.rs + } + deleted_entry & deleted_blobs } else { false From 7257e2fee7e9d966ca3b7fbc11a6c6506c49de1b Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 21 Aug 2024 11:22:24 +0300 Subject: [PATCH 077/125] fix(homeserver): /events/ iterator and write unit test in pubky --- .../src/database/tables/entries.rs | 4 +- .../src/database/tables/events.rs | 4 + pubky-homeserver/src/routes/feed.rs | 10 +- pubky/src/shared/public.rs | 106 +++++++++++++++++- 4 files changed, 119 insertions(+), 5 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index e515941..1b72274 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -91,10 +91,10 @@ impl DB { if path.starts_with("pub/") { let url = format!("pubky://{key}"); - let event = Event::put(&url); + let event = Event::delete(&url); let value = event.serialize(); - let key = entry.timestamp.to_string(); + let key = Timestamp::now().to_string(); self.tables.events.put(&mut wtxn, &key, &value)?; diff --git a/pubky-homeserver/src/database/tables/events.rs b/pubky-homeserver/src/database/tables/events.rs index 18173dc..cf82e18 100644 --- a/pubky-homeserver/src/database/tables/events.rs +++ b/pubky-homeserver/src/database/tables/events.rs @@ -26,6 +26,10 @@ impl Event { Self::Put(url.to_string()) } + pub fn delete(url: &str) -> Self { + Self::Delete(url.to_string()) + } + pub fn serialize(&self) -> Vec { to_allocvec(self).expect("Session::serialize") } diff --git a/pubky-homeserver/src/routes/feed.rs b/pubky-homeserver/src/routes/feed.rs index 734d239..bd426f3 100644 --- a/pubky-homeserver/src/routes/feed.rs +++ b/pubky-homeserver/src/routes/feed.rs @@ -30,15 +30,21 @@ pub async fn feed( .map(|c| c.as_str()) .unwrap_or("0000000000000"); + // Guard against bad cursor if cursor.len() < 13 { cursor = "0000000000000" } let mut result: Vec = vec![]; - let mut next_cursor = "".to_string(); + let mut next_cursor = cursor.to_string(); for _ in 0..limit { - match state.db.tables.events.get_greater_than(&txn, cursor)? { + match state + .db + .tables + .events + .get_greater_than(&txn, &next_cursor)? + { Some((timestamp, event_bytes)) => { let event = Event::deserialize(event_bytes)?; diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 7dbb18e..1fc7e4f 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -101,7 +101,7 @@ mod tests { use pkarr::{mainline::Testnet, Keypair}; use pubky_homeserver::Homeserver; - use reqwest::StatusCode; + use reqwest::{Method, StatusCode}; #[tokio::test] async fn put_get_delete() { @@ -627,4 +627,108 @@ mod tests { ); } } + + #[tokio::test] + async fn list_events() { + let testnet = Testnet::new(10); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + let urls = vec![ + format!("pubky://{}/pub/a.com/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{}/pub/example.con/d.txt", keypair.public_key()), + format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{}/pub/file", keypair.public_key()), + format!("pubky://{}/pub/file2", keypair.public_key()), + format!("pubky://{}/pub/z.com/a.txt", keypair.public_key()), + ]; + + for url in urls { + client.put(url.as_str(), &[0]).await.unwrap(); + client.delete(url.as_str()).await.unwrap(); + } + + let feed_url = format!("http://localhost:{}/events/", server.port()); + let feed_url = feed_url.as_str(); + + let client = PubkyClient::test(&testnet); + + let cursor; + + { + let response = client + .request( + Method::GET, + format!("{feed_url}?limit=10").as_str().try_into().unwrap(), + ) + .send() + .await + .unwrap(); + + let text = response.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + cursor = lines.last().unwrap().split(" ").last().unwrap().to_string(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{}/pub/a.com/a.txt", keypair.public_key()), + format!("DEL pubky://{}/pub/a.com/a.txt", keypair.public_key()), + format!("PUT pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("DEL pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("PUT pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("DEL pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("PUT pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("DEL pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("PUT pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("DEL pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("cursor: {cursor}",) + ] + ); + } + + { + let response = client + .request( + Method::GET, + format!("{feed_url}?limit=10&cursor={cursor}") + .as_str() + .try_into() + .unwrap(), + ) + .send() + .await + .unwrap(); + + let text = response.text().await.unwrap(); + let lines = text.split('\n').collect::>(); + + assert_eq!( + lines, + vec![ + format!("PUT pubky://{}/pub/example.con/d.txt", keypair.public_key()), + format!("DEL pubky://{}/pub/example.con/d.txt", keypair.public_key()), + format!("PUT pubky://{}/pub/example.con", keypair.public_key()), + format!("DEL pubky://{}/pub/example.con", keypair.public_key()), + format!("PUT pubky://{}/pub/file", keypair.public_key()), + format!("DEL pubky://{}/pub/file", keypair.public_key()), + format!("PUT pubky://{}/pub/file2", keypair.public_key()), + format!("DEL pubky://{}/pub/file2", keypair.public_key()), + format!("PUT pubky://{}/pub/z.com/a.txt", keypair.public_key()), + format!("DEL pubky://{}/pub/z.com/a.txt", keypair.public_key()), + lines.last().unwrap().to_string() + ] + ) + } + } } From 01d0f3bce1752ad72e823270f0f522c8f005c8b1 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 21 Aug 2024 11:28:03 +0300 Subject: [PATCH 078/125] refactor(tests): redundant keypair.public_key() calls --- pubky/src/shared/public.rs | 231 +++++++++++++++++-------------------- 1 file changed, 108 insertions(+), 123 deletions(-) diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 1fc7e4f..2f9e73b 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -207,25 +207,24 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); + let pubky = keypair.public_key(); + let urls = vec![ - format!("pubky://{}/pub/a.wrong/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.wrong/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!("pubky://{}/pub/z.wrong/a.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/a.wrong/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.wrong/a.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/z.wrong/a.txt"), ]; for url in urls { client.put(url.as_str(), &[0]).await.unwrap(); } - let url = format!("pubky://{}/pub/example.com/extra", keypair.public_key()); + let url = format!("pubky://{pubky}/pub/example.com/extra"); let url = url.as_str(); { @@ -234,14 +233,11 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), ], "normal list with no limit or cursor" ); @@ -253,8 +249,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), ], "normal list with limit but no cursor" ); @@ -273,8 +269,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "normal list with limit and a file cursor" ); @@ -293,11 +289,8 @@ mod tests { assert_eq!( list, vec![ - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), ], "normal list with limit and a directory cursor" ); @@ -308,10 +301,7 @@ mod tests { .list(url) .unwrap() .limit(2) - .cursor(&format!( - "pubky://{}/pub/example.com/a.txt", - keypair.public_key() - )) + .cursor(&format!("pubky://{pubky}/pub/example.com/a.txt")) .send() .await .unwrap(); @@ -319,8 +309,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "normal list with limit and a full url cursor" ); @@ -339,8 +329,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "normal list with limit and a leading / cursor" ); @@ -358,14 +348,11 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), ], "reverse list with no limit or cursor" ); @@ -384,11 +371,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), ], "reverse list with limit but no cursor" ); @@ -408,11 +392,8 @@ mod tests { assert_eq!( list, vec![ - format!( - "pubky://{}/pub/example.com/cc-nested/z.txt", - keypair.public_key() - ), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), ], "reverse list with limit and cursor" ); @@ -430,24 +411,26 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); + let pubky = keypair.public_key(); + let urls = vec![ - format!("pubky://{}/pub/a.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!("pubky://{}/pub/example.con/d.txt", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), - format!("pubky://{}/pub/z.com/a.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/a.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.con/d.txt"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/a.txt"), ]; for url in urls { client.put(url.as_str(), &[0]).await.unwrap(); } - let url = format!("pubky://{}/pub/", keypair.public_key()); + let url = format!("pubky://{pubky}/pub/"); let url = url.as_str(); { @@ -462,13 +445,13 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/a.com/", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), - format!("pubky://{}/pub/z.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/a.com/"), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/"), ], "normal list shallow" ); @@ -487,8 +470,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/a.com/", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/a.com/"), + format!("pubky://{pubky}/pub/example.com/"), ], "normal list shallow with limit but no cursor" ); @@ -508,8 +491,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.com/", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/example.con"), ], "normal list shallow with limit and a file cursor" ); @@ -529,9 +512,9 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/file"), ], "normal list shallow with limit and a directory cursor" ); @@ -550,13 +533,13 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/z.com/", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), - format!("pubky://{}/pub/a.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/z.com/"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/example.con/"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.com/"), + format!("pubky://{pubky}/pub/a.com/"), ], "reverse list shallow" ); @@ -576,8 +559,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/z.com/", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), + format!("pubky://{pubky}/pub/z.com/"), + format!("pubky://{pubky}/pub/file2"), ], "reverse list shallow with limit but no cursor" ); @@ -598,8 +581,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/example.con/", keypair.public_key()), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/example.con/"), ], "reverse list shallow with limit and a file cursor" ); @@ -620,8 +603,8 @@ mod tests { assert_eq!( list, vec![ - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/example.com/", keypair.public_key()), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/example.com/"), ], "reverse list shallow with limit and a directory cursor" ); @@ -639,17 +622,19 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); + let pubky = keypair.public_key(); + let urls = vec![ - format!("pubky://{}/pub/a.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!("pubky://{}/pub/example.con/d.txt", keypair.public_key()), - format!("pubky://{}/pub/example.con", keypair.public_key()), - format!("pubky://{}/pub/file", keypair.public_key()), - format!("pubky://{}/pub/file2", keypair.public_key()), - format!("pubky://{}/pub/z.com/a.txt", keypair.public_key()), + format!("pubky://{pubky}/pub/a.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/a.txt"), + format!("pubky://{pubky}/pub/example.com/b.txt"), + format!("pubky://{pubky}/pub/example.com/c.txt"), + format!("pubky://{pubky}/pub/example.com/d.txt"), + format!("pubky://{pubky}/pub/example.con/d.txt"), + format!("pubky://{pubky}/pub/example.con"), + format!("pubky://{pubky}/pub/file"), + format!("pubky://{pubky}/pub/file2"), + format!("pubky://{pubky}/pub/z.com/a.txt"), ]; for url in urls { @@ -682,16 +667,16 @@ mod tests { assert_eq!( lines, vec![ - format!("PUT pubky://{}/pub/a.com/a.txt", keypair.public_key()), - format!("DEL pubky://{}/pub/a.com/a.txt", keypair.public_key()), - format!("PUT pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("DEL pubky://{}/pub/example.com/a.txt", keypair.public_key()), - format!("PUT pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("DEL pubky://{}/pub/example.com/b.txt", keypair.public_key()), - format!("PUT pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("DEL pubky://{}/pub/example.com/c.txt", keypair.public_key()), - format!("PUT pubky://{}/pub/example.com/d.txt", keypair.public_key()), - format!("DEL pubky://{}/pub/example.com/d.txt", keypair.public_key()), + format!("PUT pubky://{pubky}/pub/a.com/a.txt"), + format!("DEL pubky://{pubky}/pub/a.com/a.txt"), + format!("PUT pubky://{pubky}/pub/example.com/a.txt"), + format!("DEL pubky://{pubky}/pub/example.com/a.txt"), + format!("PUT pubky://{pubky}/pub/example.com/b.txt"), + format!("DEL pubky://{pubky}/pub/example.com/b.txt"), + format!("PUT pubky://{pubky}/pub/example.com/c.txt"), + format!("DEL pubky://{pubky}/pub/example.com/c.txt"), + format!("PUT pubky://{pubky}/pub/example.com/d.txt"), + format!("DEL pubky://{pubky}/pub/example.com/d.txt"), format!("cursor: {cursor}",) ] ); @@ -716,16 +701,16 @@ mod tests { assert_eq!( lines, vec![ - format!("PUT pubky://{}/pub/example.con/d.txt", keypair.public_key()), - format!("DEL pubky://{}/pub/example.con/d.txt", keypair.public_key()), - format!("PUT pubky://{}/pub/example.con", keypair.public_key()), - format!("DEL pubky://{}/pub/example.con", keypair.public_key()), - format!("PUT pubky://{}/pub/file", keypair.public_key()), - format!("DEL pubky://{}/pub/file", keypair.public_key()), - format!("PUT pubky://{}/pub/file2", keypair.public_key()), - format!("DEL pubky://{}/pub/file2", keypair.public_key()), - format!("PUT pubky://{}/pub/z.com/a.txt", keypair.public_key()), - format!("DEL pubky://{}/pub/z.com/a.txt", keypair.public_key()), + format!("PUT pubky://{pubky}/pub/example.con/d.txt"), + format!("DEL pubky://{pubky}/pub/example.con/d.txt"), + format!("PUT pubky://{pubky}/pub/example.con"), + format!("DEL pubky://{pubky}/pub/example.con"), + format!("PUT pubky://{pubky}/pub/file"), + format!("DEL pubky://{pubky}/pub/file"), + format!("PUT pubky://{pubky}/pub/file2"), + format!("DEL pubky://{pubky}/pub/file2"), + format!("PUT pubky://{pubky}/pub/z.com/a.txt"), + format!("DEL pubky://{pubky}/pub/z.com/a.txt"), lines.last().unwrap().to_string() ] ) From ec4ef0d0a1947b884ab61e3a84e44f9ff9302a04 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 21 Aug 2024 18:42:45 +0300 Subject: [PATCH 079/125] feat(homeserver): log testnet bootstrap --- pubky-homeserver/src/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 8fea08b..110ed05 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use pkarr::Keypair; +use tracing::info; // use serde::{Deserialize, Serialize}; use std::{fmt::Debug, path::PathBuf, time::Duration}; @@ -41,6 +42,7 @@ impl Config { /// Testnet configurations pub fn testnet() -> Self { let testnet = pkarr::mainline::Testnet::new(10); + info!(?testnet.bootstrap, "Testnet Config"); let bootstrap = Some(testnet.bootstrap.to_owned()); let storage = Some( From a16569b23b92b508469e5aabfb23423605e2e484 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 22 Aug 2024 14:30:02 +0300 Subject: [PATCH 080/125] fix(pubky): decrypt_recovery_file, correctly parse SnD --- pubky/src/shared/recovery_file.rs | 43 +++++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/pubky/src/shared/recovery_file.rs b/pubky/src/shared/recovery_file.rs index 9fff885..4bcbc27 100644 --- a/pubky/src/shared/recovery_file.rs +++ b/pubky/src/shared/recovery_file.rs @@ -10,30 +10,33 @@ static SPEC_LINE: &str = "pubky.org/recovery"; pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; - let mut split = recovery_file.split(|byte| byte == &10); + let newline_index = recovery_file + .iter() + .position(|&r| r == 10) + .ok_or(()) + .map_err(|_| Error::RecoveryFileMissingSpecLine)?; - match split.next() { - Some(bytes) => { - if !(bytes.starts_with(SPEC_LINE.as_bytes()) - || bytes.starts_with(b"pkarr.org/recovery")) - { - return Err(Error::RecoveryFileVersionNotSupported); - } - } - None => return Err(Error::RecoveryFileMissingSpecLine), + let spec_line = &recovery_file[..newline_index]; + + if !(spec_line.starts_with(SPEC_LINE.as_bytes()) + || spec_line.starts_with(b"pkarr.org/recovery")) + { + return Err(Error::RecoveryFileVersionNotSupported); + } + + let encrypted = &recovery_file[newline_index + 1..]; + + if encrypted.is_empty() { + return Err(Error::RecoverFileMissingEncryptedSecretKey); }; - if let Some(encrypted) = split.next() { - let decrypted = decrypt(encrypted, &encryption_key)?; - let length = decrypted.len(); - let secret_key: [u8; 32] = decrypted - .try_into() - .map_err(|_| Error::RecoverFileInvalidSecretKeyLength(length))?; + let decrypted = decrypt(encrypted, &encryption_key)?; + let length = decrypted.len(); + let secret_key: [u8; 32] = decrypted + .try_into() + .map_err(|_| Error::RecoverFileInvalidSecretKeyLength(length))?; - return Ok(Keypair::from_secret_key(&secret_key)); - }; - - Err(Error::RecoverFileMissingEncryptedSecretKey) + Ok(Keypair::from_secret_key(&secret_key)) } pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { From 2f33e557a31aefdfc616a43563a3b322962e7e06 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 23 Aug 2024 08:25:34 +0300 Subject: [PATCH 081/125] feat(pubky): support resolving Endpoints with HTTPS records --- .../src/database/tables/entries.rs | 4 +- pubky/src/shared/pkarr.rs | 123 ++++++++++++++++-- pubky/src/shared/public.rs | 37 +++--- 3 files changed, 131 insertions(+), 33 deletions(-) diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 22a3aa4..e82c309 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -1,7 +1,7 @@ use pkarr::PublicKey; use postcard::{from_bytes, to_allocvec}; use serde::{Deserialize, Serialize}; -use tracing::{debug, instrument}; +use tracing::instrument; use heed::{ types::{Bytes, Str}, @@ -169,8 +169,6 @@ fn next_threshold( reverse: bool, shallow: bool, ) -> String { - debug!("Fuck me!"); - format!( "{path}{file_or_directory}{}", if file_or_directory.is_empty() { diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index 01cd0fb..e624a2a 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -10,7 +10,7 @@ use crate::{ PubkyClient, }; -const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; +const MAX_ENDPOINT_RESOLUTION_RECURSION: u8 = 3; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -56,24 +56,28 @@ impl PubkyClient { .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) } - /// Resolve a service's public_key and clearnet url from a Pubky domain + /// Resolve a service's public_key and "non-pkarr url" from a Pubky domain + /// + /// "non-pkarr" url is any URL where the hostname isn't a 52 z-base32 character, + /// usually an IPv4, IPv6 or ICANN domain, but could also be any other unknown hostname. + /// + /// Recursively resolve SVCB and HTTPS endpoints, with [MAX_ENDPOINT_RESOLUTION_RECURSION] limit. pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result { let original_target = target; // TODO: cache the result of this function? let mut target = target.to_string(); - let mut homeserver_public_key = None; + let mut endpoint_public_key = None; let mut origin = target.clone(); let mut step = 0; // PublicKey is very good at extracting the Pkarr TLD from a string. while let Ok(public_key) = PublicKey::try_from(target.clone()) { - if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION { + if step >= MAX_ENDPOINT_RESOLUTION_RECURSION { break; }; - step += 1; if let Some(signed_packet) = self @@ -85,8 +89,12 @@ impl PubkyClient { let svcb = signed_packet.resource_records(&target).fold( None, |prev: Option, answer| { - if let pkarr::dns::rdata::RData::SVCB(curr) = &answer.rdata { - let curr = curr.clone(); + if let Some(svcb) = match &answer.rdata { + pkarr::dns::rdata::RData::SVCB(svcb) => Some(svcb), + pkarr::dns::rdata::RData::HTTPS(curr) => Some(&curr.0), + _ => None, + } { + let curr = svcb.clone(); if curr.priority == 0 { return Some(curr); @@ -106,7 +114,7 @@ impl PubkyClient { ); if let Some(svcb) = svcb { - homeserver_public_key = Some(public_key.clone()); + endpoint_public_key = Some(public_key.clone()); target = svcb.target.to_string(); if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { @@ -120,14 +128,14 @@ impl PubkyClient { origin.clone_from(&target); }; - if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION { + if step >= MAX_ENDPOINT_RESOLUTION_RECURSION { continue; }; } } } - if let Some(public_key) = homeserver_public_key { + if let Some(public_key) = endpoint_public_key { let url = Url::parse(&format!( "{}://{}", if origin.starts_with("localhost") { @@ -145,6 +153,7 @@ impl PubkyClient { } } +#[derive(Debug)] pub(crate) struct Endpoint { pub public_key: PublicKey, pub url: Url, @@ -155,12 +164,104 @@ mod tests { use super::*; use pkarr::{ - dns::{rdata::SVCB, Packet}, + dns::{ + rdata::{HTTPS, SVCB}, + Packet, + }, mainline::{dht::DhtSettings, Testnet}, Keypair, PkarrClient, Settings, SignedPacket, }; use pubky_homeserver::Homeserver; + #[tokio::test] + async fn resolve_endpoint_https() { + let testnet = Testnet::new(10); + + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let domain = "example.com"; + let mut target; + + // Server + { + let keypair = Keypair::random(); + + let https = HTTPS(SVCB::new(0, domain.try_into().unwrap())); + + let mut packet = Packet::new_reply(0); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "foo".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::HTTPS(https), + )); + + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + target = format!("foo.{}", keypair.public_key()); + } + + // intermediate + { + let keypair = Keypair::random(); + + let svcb = SVCB::new(0, target.as_str().try_into().unwrap()); + + let mut packet = Packet::new_reply(0); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "bar".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + target = format!("bar.{}", keypair.public_key()) + } + + { + let keypair = Keypair::random(); + + let svcb = SVCB::new(0, target.as_str().try_into().unwrap()); + + let mut packet = Packet::new_reply(0); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + target = format!("pubky.{}", keypair.public_key()) + } + + let client = PubkyClient::test(&testnet); + + let endpoint = client.resolve_endpoint(&target).await.unwrap(); + + assert_eq!(endpoint.url.host_str().unwrap(), domain); + } + #[tokio::test] async fn resolve_homeserver() { let testnet = Testnet::new(10); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 7dbb18e..dabfdd8 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -63,32 +63,31 @@ impl PubkyClient { pub(crate) async fn pubky_to_http>(&self, url: T) -> Result { let original_url: Url = url.try_into().map_err(|_| Error::InvalidUrl)?; - if original_url.scheme() != "pubky" { - return Ok(original_url); - } - let pubky = original_url .host_str() - .ok_or(Error::Generic("Missing Pubky Url host".to_string()))? - .to_string(); + .ok_or(Error::Generic("Missing Pubky Url host".to_string()))?; - let Endpoint { mut url, .. } = self - .resolve_pubky_homeserver(&PublicKey::try_from(pubky.clone())?) - .await?; + if let Ok(public_key) = PublicKey::try_from(pubky) { + let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(&public_key).await?; - let path = original_url.path_segments(); + // TODO: remove if we move to subdomains instead of paths. + if original_url.scheme() == "pubky" { + let path = original_url.path_segments(); - // TODO: replace if we move to subdomains instead of paths. - let mut split = url.path_segments_mut().unwrap(); - split.push(&pubky); - if let Some(segments) = path { - for segment in segments { - split.push(segment); + let mut split = url.path_segments_mut().unwrap(); + split.push(pubky); + if let Some(segments) = path { + for segment in segments { + split.push(segment); + } + } + drop(split); } - } - drop(split); - Ok(url) + return Ok(url); + } + + Ok(original_url) } } From 190d3843343af98ac58c0c41c775b50934a69051 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 23 Aug 2024 10:45:29 +0300 Subject: [PATCH 082/125] feat(homeserver): use config.toml --- Cargo.lock | 82 ++++++++++++++++++++++++++++++++ pubky-homeserver/Cargo.toml | 2 + pubky-homeserver/README.md | 23 +++++++++ pubky-homeserver/src/config.rs | 64 +++++++++++++++++-------- pubky-homeserver/src/config.toml | 8 ++++ pubky-homeserver/src/main.rs | 12 ++++- pubky-homeserver/src/pkarr.rs | 2 + pubky-homeserver/src/server.rs | 5 +- 8 files changed, 175 insertions(+), 23 deletions(-) create mode 100644 pubky-homeserver/README.md create mode 100644 pubky-homeserver/src/config.toml diff --git a/Cargo.lock b/Cargo.lock index 040fe82..65b7b0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -659,6 +659,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -820,6 +826,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "headers" version = "0.4.0" @@ -914,6 +926,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.1.0" @@ -1020,6 +1038,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "inout" version = "0.1.3" @@ -1505,11 +1533,13 @@ dependencies = [ "flume", "futures-util", "heed", + "hex", "pkarr", "postcard", "pubky-common", "serde", "tokio", + "toml", "tower-cookies", "tower-http", "tracing", @@ -1780,6 +1810,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2075,6 +2114,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2523,6 +2596,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index eaba493..68323b9 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -14,11 +14,13 @@ dirs-next = "2.0.0" flume = "0.11.0" futures-util = "0.3.30" heed = "0.20.3" +hex = "0.4.3" pkarr = { version = "2.1.0", features = ["async"] } postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } serde = { version = "1.0.204", features = ["derive"] } tokio = { version = "1.37.0", features = ["full"] } +toml = "0.8.19" tower-cookies = "0.10.0" tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" diff --git a/pubky-homeserver/README.md b/pubky-homeserver/README.md new file mode 100644 index 0000000..d1799a2 --- /dev/null +++ b/pubky-homeserver/README.md @@ -0,0 +1,23 @@ +# Pubky Homeserver + +## Usage + +Use `cargo run` + +```bash +cargo run -- --config=./src/config.toml +``` + +Or Build first then run from target. + +Build + +```bash +cargo build --release +``` + +Run with an optional config file + +```bash +../target/release/pubky-homeserver --config=./src/config.toml +``` diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 110ed05..e177311 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -1,10 +1,14 @@ //! Configuration for the server -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use pkarr::Keypair; +use serde::{Deserialize, Deserializer, Serialize}; +use std::{ + fmt::Debug, + path::{Path, PathBuf}, + time::Duration, +}; use tracing::info; -// use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, path::PathBuf, time::Duration}; use pubky_common::timestamp::Timestamp; @@ -12,10 +16,7 @@ const DEFAULT_HOMESERVER_PORT: u16 = 6287; const DEFAULT_STORAGE_DIR: &str = "pubky"; /// Server configuration -#[derive( - // Serialize, Deserialize, - Clone, -)] +#[derive(Serialize, Deserialize, Clone)] pub struct Config { port: Option, bootstrap: Option>, @@ -24,20 +25,22 @@ pub struct Config { /// /// Defaults to a directory in the OS data directory storage: Option, - keypair: Keypair, + #[serde(deserialize_with = "secret_key_deserialize")] + secret_key: Option<[u8; 32]>, dht_request_timeout: Option, } impl Config { - // /// Load the config from a file. - // pub async fn load(path: impl AsRef) -> Result { - // let s = tokio::fs::read_to_string(path.as_ref()) - // .await - // .with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?; - // let config: Config = toml::from_str(&s)?; - // Ok(config) - // } + /// Load the config from a file. + pub async fn load(path: impl AsRef) -> Result { + let s = tokio::fs::read_to_string(path.as_ref()) + .await + .with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?; + + let config: Config = toml::from_str(&s)?; + Ok(config) + } /// Testnet configurations pub fn testnet() -> Self { @@ -55,7 +58,6 @@ impl Config { bootstrap, storage, port: Some(15411), - keypair: Keypair::from_secret_key(&[0_u8; 32]), dht_request_timeout: Some(Duration::from_millis(10)), ..Default::default() } @@ -103,8 +105,8 @@ impl Config { Ok(dir.join("homeserver")) } - pub fn keypair(&self) -> &Keypair { - &self.keypair + pub fn keypair(&self) -> Keypair { + Keypair::from_secret_key(&self.secret_key.unwrap_or_default()) } pub(crate) fn dht_request_timeout(&self) -> Option { @@ -119,12 +121,34 @@ impl Default for Config { bootstrap: None, domain: "localhost".to_string(), storage: None, - keypair: Keypair::random(), + secret_key: None, dht_request_timeout: None, } } } +fn secret_key_deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt: Option = Option::deserialize(deserializer)?; + + match opt { + Some(s) => { + let bytes = hex::decode(s).map_err(serde::de::Error::custom)?; + + if bytes.len() != 32 { + return Err(serde::de::Error::custom("Expected a 32-byte array")); + } + + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + Ok(Some(arr)) + } + None => Ok(None), + } +} + impl Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_map() diff --git a/pubky-homeserver/src/config.toml b/pubky-homeserver/src/config.toml new file mode 100644 index 0000000..dda26e9 --- /dev/null +++ b/pubky-homeserver/src/config.toml @@ -0,0 +1,8 @@ +# Secret key (in hex) to generate the Homeserver's Keypair +secret_key = "0000000000000000000000000000000000000000000000000000000000000000" +# Domain to be published in Pkarr records for this server to be accessible by. +domain = "localhost" +# Port for the Homeserver to listen on. +port = 6287 +# Storage directory Defaults to +# storage = "" diff --git a/pubky-homeserver/src/main.rs b/pubky-homeserver/src/main.rs index 2a17bda..dad25df 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use anyhow::Result; use pubky_homeserver::{config::Config, Homeserver}; @@ -8,8 +10,14 @@ struct Cli { /// [tracing_subscriber::EnvFilter] #[clap(short, long)] tracing_env_filter: Option, + + /// Run Homeserver in a local testnet #[clap(long)] testnet: bool, + + /// Optional Path to config file. + #[clap(short, long)] + config: Option, } #[tokio::main] @@ -25,8 +33,10 @@ async fn main() -> Result<()> { let server = Homeserver::start(if args.testnet { Config::testnet() + } else if let Some(config_path) = args.config { + Config::load(config_path).await? } else { - Default::default() + Config::default() }) .await?; diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs index 113c598..cf4d7b7 100644 --- a/pubky-homeserver/src/pkarr.rs +++ b/pubky-homeserver/src/pkarr.rs @@ -11,6 +11,8 @@ pub async fn publish_server_packet( domain: &str, port: u16, ) -> anyhow::Result<()> { + // TODO: Try to resolve first before publishing. + let mut packet = Packet::new_reply(0); let mut svcb = SVCB::new(0, domain.try_into()?); diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 3db7441..cdc352c 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -30,7 +30,8 @@ impl Homeserver { pub async fn start(config: Config) -> Result { debug!(?config); - let public_key = config.keypair().public_key(); + let keypair = config.keypair(); + let public_key = keypair.public_key(); let db = DB::open(&config.storage()?)?; @@ -72,7 +73,7 @@ impl Homeserver { info!("Homeserver listening on http://localhost:{port}"); - publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?; + publish_server_packet(pkarr_client, &keypair, config.domain(), port).await?; info!("Homeserver listening on pubky://{public_key}"); From 7eecfb3fa79e3a5d1cbc6c078294aef178968630 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 23 Aug 2024 11:17:54 +0300 Subject: [PATCH 083/125] feat(homeserver): add testnet option to config.toml --- pubky-homeserver/src/config.rs | 15 ++++++++++++++- pubky-homeserver/src/config.toml | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index e177311..55f015c 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -18,6 +18,7 @@ const DEFAULT_STORAGE_DIR: &str = "pubky"; /// Server configuration #[derive(Serialize, Deserialize, Clone)] pub struct Config { + testnet: bool, port: Option, bootstrap: Option>, domain: String, @@ -39,13 +40,23 @@ impl Config { .with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?; let config: Config = toml::from_str(&s)?; + + if config.testnet { + let testnet_config = Config::testnet(); + + return Ok(Config { + bootstrap: testnet_config.bootstrap, + ..config + }); + } + Ok(config) } /// Testnet configurations pub fn testnet() -> Self { let testnet = pkarr::mainline::Testnet::new(10); - info!(?testnet.bootstrap, "Testnet Config"); + info!(?testnet.bootstrap, "Testnet bootstrap nodes"); let bootstrap = Some(testnet.bootstrap.to_owned()); let storage = Some( @@ -117,6 +128,7 @@ impl Config { impl Default for Config { fn default() -> Self { Self { + testnet: false, port: Some(0), bootstrap: None, domain: "localhost".to_string(), @@ -152,6 +164,7 @@ where impl Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_map() + .entry(&"testnet", &self.testnet) .entry(&"port", &self.port()) .entry(&"storage", &self.storage()) .entry(&"public_key", &self.keypair().public_key()) diff --git a/pubky-homeserver/src/config.toml b/pubky-homeserver/src/config.toml index dda26e9..2012efc 100644 --- a/pubky-homeserver/src/config.toml +++ b/pubky-homeserver/src/config.toml @@ -1,3 +1,5 @@ +# Use testnet network (local DHT) for testing. +testnet = false # Secret key (in hex) to generate the Homeserver's Keypair secret_key = "0000000000000000000000000000000000000000000000000000000000000000" # Domain to be published in Pkarr records for this server to be accessible by. From 620e2ad2ee6e25f26b16ab2136bb94c1b05bbbd2 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 25 Aug 2024 11:54:24 +0300 Subject: [PATCH 084/125] feat(pubky): add PubkyClientBuilder --- pubky/src/native.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 0ca2d7c..547f8db 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -27,6 +27,33 @@ impl Default for PubkyClient { } } +#[derive(Debug, Default)] +pub struct PubkyClientBuilder { + pkarr_settings: Option, +} + +impl PubkyClientBuilder { + /// Set Pkarr client [pkarr::Settings]. + pub fn pkarr_settings(mut self, settings: pkarr::Settings) -> Self { + self.pkarr_settings = settings.into(); + self + } + + /// Build [PubkyClient] + pub fn build(self) -> PubkyClient { + PubkyClient { + http: reqwest::Client::builder() + .cookie_store(true) + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(), + pkarr: PkarrClient::new(self.pkarr_settings.unwrap_or_default()) + .unwrap() + .as_async(), + } + } +} + // === Public API === impl PubkyClient { @@ -37,11 +64,15 @@ impl PubkyClient { .user_agent(DEFAULT_USER_AGENT) .build() .unwrap(), - #[cfg(not(target_arch = "wasm32"))] pkarr: PkarrClient::new(Default::default()).unwrap().as_async(), } } + /// Returns a builder to edit settings before creating [PubkyClient]. + pub fn builder() -> PubkyClientBuilder { + PubkyClientBuilder::default() + } + pub fn test(testnet: &Testnet) -> Self { Self { http: reqwest::Client::builder() From ab7c005d670e44e5fb7cce218bc9931394db74d8 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 26 Aug 2024 14:24:37 +0300 Subject: [PATCH 085/125] feat(common): add serde to Timestamp --- pubky-common/Cargo.toml | 3 ++ pubky-common/src/timestamp.rs | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 0a9df3b..9e539b7 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -19,3 +19,6 @@ crypto_secretbox = { version = "0.1.1", features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.69" + +[dev-dependencies] +postcard = "1.0.8" diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index 4c546d5..4b9accc 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -6,6 +6,12 @@ use std::{ sync::Mutex, }; +use serde::{ + de::{SeqAccess, Visitor}, + ser::SerializeTuple, + Deserialize, Deserializer, Serialize, Serializer, +}; + use once_cell::sync::Lazy; use rand::Rng; @@ -173,6 +179,58 @@ pub fn system_time() -> u64 { * 1000 } +// === Serde === + +impl Serialize for Timestamp { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Convert u64 to 8 bytes in Big-Endian format + let mut tup = serializer.serialize_tuple(8)?; + + for byte in self.to_bytes() { + tup.serialize_element(&byte)?; + } + + tup.end() + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TimestampVisitor; + + impl<'de> Visitor<'de> for TimestampVisitor { + type Value = Timestamp; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a tuple of 8 bytes") + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: SeqAccess<'de>, + { + let mut bytes = [0u8; 8]; + + for i in 0..8 { + bytes[i] = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?; + } + + Ok(Timestamp::from(bytes)) + } + } + + deserializer.deserialize_tuple(8, TimestampVisitor) + } +} + #[derive(thiserror::Error, Debug)] pub enum TimestampError { #[error("Invalid bytes length, Timestamp should be encoded as 8 bytes, got {0}")] @@ -237,4 +295,17 @@ mod tests { assert_eq!(decoded, timestamp) } + + #[test] + fn serde() { + let timestamp = Timestamp::now(); + + let serialized = postcard::to_allocvec(×tamp).unwrap(); + + assert_eq!(serialized, timestamp.to_bytes()); + + let deserialized: Timestamp = postcard::from_bytes(&serialized).unwrap(); + + assert_eq!(deserialized, timestamp); + } } From 19dd3cb2ba1f08edef327edd752113f4f8a37a2f Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 26 Aug 2024 14:24:37 +0300 Subject: [PATCH 086/125] feat(common): add serde to Timestamp --- Cargo.lock | 1 + pubky-common/Cargo.toml | 13 +++++++++++- pubky-common/src/timestamp.rs | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 65b7b0b..72bffff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,6 +635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", + "serde", "signature", ] diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 0a9df3b..202a815 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -14,8 +14,19 @@ pkarr = "2.1.0" rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } -serde = { version = "1.0.204", features = ["derive"] } crypto_secretbox = { version = "0.1.1", features = ["std"] } +serde = { version = "1.0.204", features = ["derive"], optional = true } + [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.69" + +[dev-dependencies] +postcard = "1.0.8" + +[features] + +serde = ["dep:serde", "ed25519-dalek/serde"] +full = ['serde'] + +default = ['full'] diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index 4c546d5..0235f66 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -1,5 +1,6 @@ //! Monotonic unix timestamp in microseconds +use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::{ ops::{Add, Sub}, @@ -83,6 +84,12 @@ impl Timestamp { } } +impl Default for Timestamp { + fn default() -> Self { + Timestamp::now() + } +} + impl Display for Timestamp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let bytes: [u8; 8] = self.into(); @@ -155,6 +162,26 @@ impl Sub for &Timestamp { } } +impl Serialize for Timestamp { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bytes = self.to_bytes(); + bytes.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes: [u8; 8] = Deserialize::deserialize(deserializer)?; + Ok(Timestamp(u64::from_be_bytes(bytes))) + } +} + #[cfg(not(target_arch = "wasm32"))] /// Return the number of microseconds since [SystemTime::UNIX_EPOCH] fn system_time() -> u64 { @@ -237,4 +264,17 @@ mod tests { assert_eq!(decoded, timestamp) } + + #[test] + fn serde() { + let timestamp = Timestamp::now(); + + let serialized = postcard::to_allocvec(×tamp).unwrap(); + + assert_eq!(serialized, timestamp.to_bytes()); + + let deserialized: Timestamp = postcard::from_bytes(&serialized).unwrap(); + + assert_eq!(deserialized, timestamp); + } } From aaf01cf2d77d0d192b882d8c1ee06c63c9b6fee6 Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 27 Aug 2024 17:15:25 +0300 Subject: [PATCH 087/125] feat(common): add AuthToken with scopes --- Cargo.lock | 26 ++-- Cargo.toml | 4 + pubky-common/Cargo.toml | 6 +- pubky-common/src/auth.rs | 231 ++++++++++++++++++++++++++++++---- pubky-common/src/timestamp.rs | 4 +- pubky-homeserver/Cargo.toml | 6 +- pubky/Cargo.toml | 6 +- 7 files changed, 235 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72bffff..df600ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,9 +336,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" @@ -1401,10 +1401,10 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4548c673cbf8c91b69f7a17d3a042710aa73cffe5e82351db5378f26c3be64d8" +version = "2.2.0" +source = "git+https://github.com/Pubky/pkarr?branch=v3#17975121c809d97dcad907fbb2ffc782e994d270" dependencies = [ + "base32", "bytes", "document-features", "dyn-clone", @@ -1416,13 +1416,13 @@ dependencies = [ "mainline", "rand", "self_cell", + "serde", "simple-dns", "thiserror", "tracing", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "z32", ] [[package]] @@ -1753,9 +1753,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -1781,9 +1781,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -2616,12 +2616,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "z32" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc" - [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 9e2e527..4fcef4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,10 @@ members = [ "pubky","pubky-*"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" +[workspace.dependencies] +pkarr = { git = "https://github.com/Pubky/pkarr", branch = "v3", package = "pkarr", features = ["async"] } +serde = { version = "^1.0.209", features = ["derive"] } + [profile.release] lto = true opt-level = 'z' diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 202a815..675ec65 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -10,13 +10,13 @@ base32 = "0.5.0" blake3 = "1.5.1" ed25519-dalek = "2.1.1" once_cell = "1.19.0" -pkarr = "2.1.0" +pkarr = { workspace = true } rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } crypto_secretbox = { version = "0.1.1", features = ["std"] } -serde = { version = "1.0.204", features = ["derive"], optional = true } +serde = { workspace = true, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.69" @@ -26,7 +26,7 @@ postcard = "1.0.8" [features] -serde = ["dep:serde", "ed25519-dalek/serde"] +serde = ["dep:serde", "ed25519-dalek/serde", "pkarr/serde"] full = ['serde'] default = ['full'] diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 5d5ebba..164a124 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -3,6 +3,7 @@ use std::sync::{Arc, Mutex}; use ed25519_dalek::ed25519::SignatureBytes; +use serde::{Deserialize, Serialize}; use crate::{ crypto::{random_hash, Keypair, PublicKey, Signature}, @@ -12,9 +13,184 @@ use crate::{ // 30 seconds const TIME_INTERVAL: u64 = 30 * 1_000_000; +const CURRENT_VERSION: u8 = 0; +// 45 seconds in the past or the future +const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000; + #[derive(Debug, PartialEq)] pub struct AuthnSignature(Box<[u8]>); +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct AuthToken { + /// Version of the [AuthToken]. + /// + /// Version 0: Signer is implicitly the same as the [AuthToken::subject] + version: u8, + /// The Pubky of the party verifying the [AuthToken], for example a web server. + audience: PublicKey, + /// Timestamp + timestamp: Timestamp, + /// The [PublicKey] of the owner of the resources being accessed by this token. + subject: PublicKey, + /// Signature over the token. + signature: Signature, + // Variable length scopes + scopes: Vec, +} + +impl AuthToken { + pub fn new(signer: &Keypair, audience: &PublicKey, scopes: Vec) -> Self { + let timestamp = Timestamp::now(); + + let signature = signer.sign(&AuthToken::signable(audience, ×tamp, &scopes)); + + Self { + version: 0, + subject: signer.public_key(), + audience: audience.to_owned(), + timestamp, + scopes, + signature, + } + } + + fn verify(audience: &PublicKey, bytes: &[u8]) -> Result { + if bytes[0] > CURRENT_VERSION { + return Err(Error::UnknownVersion); + } + + let token: AuthToken = postcard::from_bytes(bytes)?; + + let now = Timestamp::now(); + + match token.version { + 0 => { + if &token.audience != audience { + return Err(Error::InvalidAudience( + audience.to_string(), + token.audience.to_string(), + )); + } + + // Chcek timestamp; + let diff = token.timestamp.difference(&now); + if diff > TIMESTAMP_WINDOW { + return Err(Error::TooFarInTheFuture); + } + if diff < -TIMESTAMP_WINDOW { + return Err(Error::Expired); + } + + token + .subject + .verify( + &AuthToken::signable(&token.audience, &token.timestamp, &token.scopes), + &token.signature, + ) + .map_err(|_| Error::InvalidSignature)?; + + Ok(token) + } + _ => unreachable!(), + } + } + + fn signable(audience: &PublicKey, timestamp: &Timestamp, scopes: &Vec) -> Vec { + let serialized_scopes = &postcard::to_allocvec(&scopes).unwrap(); + + let mut signable = Vec::with_capacity(1 + 32 + 8 + serialized_scopes.len()); + + signable.extend_from_slice(&[CURRENT_VERSION]); + signable.extend_from_slice(audience.as_bytes()); + signable.extend_from_slice(×tamp.to_bytes()); + signable.extend_from_slice(serialized_scopes); + + signable + } + + /// A unique ID for this [AuthToken], which is a concatenation of + /// [AuthToken::subject] and [AuthToken::timestamp]. + /// + /// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::subject]. + pub fn id(&self) -> [u8; 40] { + let mut id = [0u8; 40]; + id[0..32].copy_from_slice(&self.subject.to_bytes()); + id[32..].copy_from_slice(&self.timestamp.to_bytes()); + + id + } + + pub fn serialize(&self) -> Result, postcard::Error> { + postcard::to_allocvec(self) + } +} + +#[derive(Debug, Clone)] +pub struct AuthVerifier { + audience: PublicKey, + seen: Arc>>, +} + +impl AuthVerifier { + pub fn new(audience: PublicKey) -> Self { + Self { + audience, + seen: Arc::new(Mutex::new(Vec::new())), + } + } + + pub fn verify(&self, bytes: &[u8]) -> Result { + self.gc(); + + let token = AuthToken::verify(&self.audience, bytes)?; + + // Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), + + let mut seen = self.seen.lock().unwrap(); + + let id = token.id(); + + match seen.binary_search_by(|element| element.cmp(&id)) { + Ok(index) | Err(index) => { + seen.insert(index, id); + } + }; + + Ok(token) + } + + // === Private Methods === + + /// Remove all tokens older than two time intervals in the past. + fn gc(&self) { + let threshold = ((Timestamp::now().into_inner() / TIME_INTERVAL) - 2).to_be_bytes(); + + let mut inner = self.seen.lock().unwrap(); + + match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) { + Ok(index) | Err(index) => { + inner.drain(0..index); + } + } + } +} + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum Error { + #[error("Unknown version")] + UnknownVersion, + #[error("Invalid audience. Expected {0}, got {1}")] + InvalidAudience(String, String), + #[error("AuthToken has a timestamp that is more than 45 seconds in the future")] + TooFarInTheFuture, + #[error("AuthToken has a timestamp that is more than 45 seconds in the past")] + Expired, + #[error("Invalid Signature")] + InvalidSignature, + #[error(transparent)] + Postcard(#[from] postcard::Error), +} + impl AuthnSignature { pub fn new(signer: &Keypair, audience: &PublicKey, token: Option<&[u8]>) -> Self { let mut bytes = Vec::with_capacity(96); @@ -183,38 +359,51 @@ pub enum AuthnSignatureError { #[cfg(test)] mod tests { - use crate::crypto::Keypair; + use crate::{auth::TIMESTAMP_WINDOW, crypto::Keypair, timestamp::Timestamp}; - use super::{AuthnSignature, AuthnVerifier}; + use super::{AuthToken, AuthVerifier, Error}; #[test] fn sign_verify() { - let keypair = Keypair::random(); - let signer = keypair.public_key(); + let signer = Keypair::random(); let audience = Keypair::random().public_key(); + let scopes = vec!["*:*".to_string()]; - let verifier = AuthnVerifier::new(audience.clone()); + let verifier = AuthVerifier::new(audience.clone()); - let authn_signature = AuthnSignature::generate(&keypair, &audience); + let token = AuthToken::new(&signer, &audience, scopes.clone()); - verifier - .verify(authn_signature.as_bytes(), &signer) - .unwrap(); + verifier.verify(&token.serialize().unwrap()).unwrap(); - { - // Invalid signable - let mut invalid = authn_signature.as_bytes().to_vec(); - invalid[64..].copy_from_slice(&[0; 32]); + assert_eq!(token.scopes, scopes) + } - assert!(verifier.verify(&invalid, &signer).is_err()) - } + #[test] + fn expired() { + let signer = Keypair::random(); + let audience = Keypair::random().public_key(); + let scopes = vec!["*:*".to_string()]; - { - // Invalid signer - let mut invalid = authn_signature.as_bytes().to_vec(); - invalid[0..32].copy_from_slice(&[0; 32]); + let verifier = AuthVerifier::new(audience.clone()); - assert!(verifier.verify(&invalid, &signer).is_err()) - } + let timestamp = (&Timestamp::now()) - (TIMESTAMP_WINDOW as u64); + + let signable = AuthToken::signable(&audience, ×tamp, &scopes); + let signature = signer.sign(&signable); + + let token = AuthToken { + version: 0, + subject: signer.public_key(), + audience, + timestamp, + signature, + scopes, + }; + + let serialized = token.serialize().unwrap(); + + let result = verifier.verify(&serialized); + + assert_eq!(result, Err(Error::Expired)); } } diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index 0235f66..848f894 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -75,8 +75,8 @@ impl Timestamp { self.0.to_be_bytes() } - pub fn difference(&self, rhs: &Timestamp) -> u64 { - self.0.abs_diff(rhs.0) + pub fn difference(&self, rhs: &Timestamp) -> i64 { + (self.0 as i64) - (rhs.0 as i64) } pub fn into_inner(&self) -> u64 { diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 68323b9..c8abfd5 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -8,17 +8,17 @@ anyhow = "1.0.82" axum = { version = "0.7.5", features = ["macros"] } axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] } base32 = "0.5.1" -bytes = "1.6.1" +bytes = "^1.7.1" clap = { version = "4.5.11", features = ["derive"] } dirs-next = "2.0.0" flume = "0.11.0" futures-util = "0.3.30" heed = "0.20.3" hex = "0.4.3" -pkarr = { version = "2.1.0", features = ["async"] } +pkarr = { workspace = true } postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } -serde = { version = "1.0.204", features = ["derive"] } +serde = { workspace = true } tokio = { version = "1.37.0", features = ["full"] } toml = "0.8.19" tower-cookies = "0.10.0" diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 4c82cfb..90039ee 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -14,17 +14,17 @@ crate-type = ["cdylib", "rlib"] thiserror = "1.0.62" wasm-bindgen = "0.2.92" url = "2.5.2" -bytes = "1.6.1" +bytes = "^1.7.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } argon2 = { version = "0.5.3", features = ["std"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -pkarr = { version="2.1.0", features = ["async"] } +pkarr = { workspace = true, features = ["async"] } reqwest = { version = "0.12.5", features = ["cookies"], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -pkarr = { version = "2.1.0", default-features = false } +pkarr = { workspace = true, default-features = false } reqwest = { version = "0.12.5", default-features = false } js-sys = "0.3.69" From f3469d34a452c8e48334d07e8e47c4de94505857 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 28 Aug 2024 15:32:25 +0300 Subject: [PATCH 088/125] feat(common): update AuthToken and AuthVerifier --- pubky-common/src/auth.rs | 121 ++++++++++++++++++++-------------- pubky-common/src/timestamp.rs | 58 ---------------- 2 files changed, 71 insertions(+), 108 deletions(-) diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 164a124..6c476c6 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -26,32 +26,36 @@ pub struct AuthToken { /// /// Version 0: Signer is implicitly the same as the [AuthToken::subject] version: u8, - /// The Pubky of the party verifying the [AuthToken], for example a web server. - audience: PublicKey, + /// Signature over the token. + signature: Signature, /// Timestamp timestamp: Timestamp, /// The [PublicKey] of the owner of the resources being accessed by this token. subject: PublicKey, - /// Signature over the token. - signature: Signature, + /// The Pubky of the party verifying the [AuthToken], for example a web server. + audience: PublicKey, // Variable length scopes scopes: Vec, } impl AuthToken { - pub fn new(signer: &Keypair, audience: &PublicKey, scopes: Vec) -> Self { + pub fn sign(signer: &Keypair, audience: &PublicKey, scopes: Vec) -> Self { let timestamp = Timestamp::now(); - let signature = signer.sign(&AuthToken::signable(audience, ×tamp, &scopes)); - - Self { + let mut token = Self { version: 0, subject: signer.public_key(), audience: audience.to_owned(), timestamp, scopes, - signature, - } + signature: Signature::from_bytes(&[0; 64]), + }; + + let serialized = token.serialize(); + + token.signature = signer.sign(&serialized[65..]); + + token } fn verify(audience: &PublicKey, bytes: &[u8]) -> Result { @@ -61,10 +65,10 @@ impl AuthToken { let token: AuthToken = postcard::from_bytes(bytes)?; - let now = Timestamp::now(); - match token.version { 0 => { + let now = Timestamp::now(); + if &token.audience != audience { return Err(Error::InvalidAudience( audience.to_string(), @@ -83,10 +87,7 @@ impl AuthToken { token .subject - .verify( - &AuthToken::signable(&token.audience, &token.timestamp, &token.scopes), - &token.signature, - ) + .verify(AuthToken::signable(token.version, bytes), &token.signature) .map_err(|_| Error::InvalidSignature)?; Ok(token) @@ -95,40 +96,33 @@ impl AuthToken { } } - fn signable(audience: &PublicKey, timestamp: &Timestamp, scopes: &Vec) -> Vec { - let serialized_scopes = &postcard::to_allocvec(&scopes).unwrap(); - - let mut signable = Vec::with_capacity(1 + 32 + 8 + serialized_scopes.len()); - - signable.extend_from_slice(&[CURRENT_VERSION]); - signable.extend_from_slice(audience.as_bytes()); - signable.extend_from_slice(×tamp.to_bytes()); - signable.extend_from_slice(serialized_scopes); - - signable + pub fn serialize(&self) -> Vec { + postcard::to_allocvec(self).unwrap() } /// A unique ID for this [AuthToken], which is a concatenation of /// [AuthToken::subject] and [AuthToken::timestamp]. /// /// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::subject]. - pub fn id(&self) -> [u8; 40] { - let mut id = [0u8; 40]; - id[0..32].copy_from_slice(&self.subject.to_bytes()); - id[32..].copy_from_slice(&self.timestamp.to_bytes()); - - id + fn id(version: u8, bytes: &[u8]) -> Box<[u8]> { + match version { + 0 => bytes[65..105].into(), + _ => unreachable!(), + } } - pub fn serialize(&self) -> Result, postcard::Error> { - postcard::to_allocvec(self) + fn signable(version: u8, bytes: &[u8]) -> &[u8] { + match version { + 0 => bytes[65..].into(), + _ => unreachable!(), + } } } #[derive(Debug, Clone)] pub struct AuthVerifier { audience: PublicKey, - seen: Arc>>, + seen: Arc>>>, } impl AuthVerifier { @@ -144,19 +138,17 @@ impl AuthVerifier { let token = AuthToken::verify(&self.audience, bytes)?; - // Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), - let mut seen = self.seen.lock().unwrap(); - let id = token.id(); + let id = AuthToken::id(token.version, bytes); match seen.binary_search_by(|element| element.cmp(&id)) { - Ok(index) | Err(index) => { - seen.insert(index, id); + Ok(_) => Err(Error::AlreadyUsed), + Err(index) => { + seen.insert(index, id.into()); + Ok(token) } - }; - - Ok(token) + } } // === Private Methods === @@ -189,6 +181,8 @@ pub enum Error { InvalidSignature, #[error(transparent)] Postcard(#[from] postcard::Error), + #[error("AuthToken already used")] + AlreadyUsed, } impl AuthnSignature { @@ -337,8 +331,8 @@ fn signable( let mut arr = [0; 115]; arr[..11].copy_from_slice(crate::namespaces::PUBKY_AUTHN); - arr[11..19].copy_from_slice(time_step_bytes); arr[19..51].copy_from_slice(signer.as_bytes()); + arr[11..19].copy_from_slice(time_step_bytes); arr[51..83].copy_from_slice(audience.as_bytes()); arr[83..].copy_from_slice(token_hash); @@ -363,6 +357,27 @@ mod tests { use super::{AuthToken, AuthVerifier, Error}; + #[test] + fn v0_id_signable() { + let signer = Keypair::random(); + let audience = Keypair::random().public_key(); + let scopes = vec!["*:*".to_string()]; + + let token = AuthToken::sign(&signer, &audience, scopes.clone()); + + let serialized = &token.serialize(); + + assert_eq!( + AuthToken::id(token.version, serialized), + serialized[65..105].into() + ); + + assert_eq!( + AuthToken::signable(token.version, serialized), + &serialized[65..] + ) + } + #[test] fn sign_verify() { let signer = Keypair::random(); @@ -371,11 +386,13 @@ mod tests { let verifier = AuthVerifier::new(audience.clone()); - let token = AuthToken::new(&signer, &audience, scopes.clone()); + let token = AuthToken::sign(&signer, &audience, scopes.clone()); - verifier.verify(&token.serialize().unwrap()).unwrap(); + let serialized = &token.serialize(); - assert_eq!(token.scopes, scopes) + verifier.verify(serialized).unwrap(); + + assert_eq!(token.scopes, scopes); } #[test] @@ -388,7 +405,11 @@ mod tests { let timestamp = (&Timestamp::now()) - (TIMESTAMP_WINDOW as u64); - let signable = AuthToken::signable(&audience, ×tamp, &scopes); + let mut signable = vec![]; + signable.extend_from_slice(signer.public_key().as_bytes()); + signable.extend_from_slice(audience.as_bytes()); + signable.extend_from_slice(&postcard::to_allocvec(&scopes).unwrap()); + let signature = signer.sign(&signable); let token = AuthToken { @@ -400,7 +421,7 @@ mod tests { scopes, }; - let serialized = token.serialize().unwrap(); + let serialized = token.serialize(); let result = verifier.verify(&serialized); diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index 5b7558e..848f894 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -7,12 +7,6 @@ use std::{ sync::Mutex, }; -use serde::{ - de::{SeqAccess, Visitor}, - ser::SerializeTuple, - Deserialize, Deserializer, Serialize, Serializer, -}; - use once_cell::sync::Lazy; use rand::Rng; @@ -206,58 +200,6 @@ pub fn system_time() -> u64 { * 1000 } -// === Serde === - -impl Serialize for Timestamp { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // Convert u64 to 8 bytes in Big-Endian format - let mut tup = serializer.serialize_tuple(8)?; - - for byte in self.to_bytes() { - tup.serialize_element(&byte)?; - } - - tup.end() - } -} - -impl<'de> Deserialize<'de> for Timestamp { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct TimestampVisitor; - - impl<'de> Visitor<'de> for TimestampVisitor { - type Value = Timestamp; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a tuple of 8 bytes") - } - - fn visit_seq(self, mut seq: V) -> Result - where - V: SeqAccess<'de>, - { - let mut bytes = [0u8; 8]; - - for i in 0..8 { - bytes[i] = seq - .next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?; - } - - Ok(Timestamp::from(bytes)) - } - } - - deserializer.deserialize_tuple(8, TimestampVisitor) - } -} - #[derive(thiserror::Error, Debug)] pub enum TimestampError { #[error("Invalid bytes length, Timestamp should be encoded as 8 bytes, got {0}")] From 98910b40dee80c3e426670ab2bbf1768613f56de Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 28 Aug 2024 19:55:29 +0300 Subject: [PATCH 089/125] feat(homeserver): update signup and signin to use new AuthToken --- pubky-common/src/auth.rs | 200 ++++------------------------ pubky-homeserver/src/error.rs | 5 +- pubky-homeserver/src/routes.rs | 4 +- pubky-homeserver/src/routes/auth.rs | 8 +- pubky-homeserver/src/server.rs | 6 +- pubky/src/shared/auth.rs | 18 +-- 6 files changed, 45 insertions(+), 196 deletions(-) diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 6c476c6..8f95fbc 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -2,11 +2,10 @@ use std::sync::{Arc, Mutex}; -use ed25519_dalek::ed25519::SignatureBytes; use serde::{Deserialize, Serialize}; use crate::{ - crypto::{random_hash, Keypair, PublicKey, Signature}, + crypto::{Keypair, PublicKey, Signature}, timestamp::Timestamp, }; @@ -17,9 +16,6 @@ const CURRENT_VERSION: u8 = 0; // 45 seconds in the past or the future const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000; -#[derive(Debug, PartialEq)] -pub struct AuthnSignature(Box<[u8]>); - #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct AuthToken { /// Version of the [AuthToken]. @@ -58,6 +54,11 @@ impl AuthToken { token } + /// Authenticate signer to an audience directly with [] capailities. + /// + /// + // pub fn authn(signer: &Keypair, audience: &PublicKey) -> Self {} + fn verify(audience: &PublicKey, bytes: &[u8]) -> Result { if bytes[0] > CURRENT_VERSION { return Err(Error::UnknownVersion); @@ -100,6 +101,10 @@ impl AuthToken { postcard::to_allocvec(self).unwrap() } + pub fn subject(&self) -> &PublicKey { + &self.subject + } + /// A unique ID for this [AuthToken], which is a concatenation of /// [AuthToken::subject] and [AuthToken::timestamp]. /// @@ -185,172 +190,6 @@ pub enum Error { AlreadyUsed, } -impl AuthnSignature { - pub fn new(signer: &Keypair, audience: &PublicKey, token: Option<&[u8]>) -> Self { - let mut bytes = Vec::with_capacity(96); - - let time: u64 = Timestamp::now().into(); - let time_step = time / TIME_INTERVAL; - - let token_hash = token.map_or(random_hash(), crate::crypto::hash); - - let signature = signer - .sign(&signable( - &time_step.to_be_bytes(), - &signer.public_key(), - audience, - token_hash.as_bytes(), - )) - .to_bytes(); - - bytes.extend_from_slice(&signature); - bytes.extend_from_slice(token_hash.as_bytes()); - - Self(bytes.into()) - } - - /// Sign a randomly generated nonce - pub fn generate(keypair: &Keypair, audience: &PublicKey) -> Self { - AuthnSignature::new(keypair, audience, None) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } -} - -#[derive(Debug, Clone)] -pub struct AuthnVerifier { - audience: PublicKey, - inner: Arc>>, - // TODO: Support permisisons - // token_hashes: HashSet<[u8; 32]>, -} - -impl AuthnVerifier { - pub fn new(audience: PublicKey) -> Self { - Self { - audience, - inner: Arc::new(Mutex::new(Vec::new())), - } - } - - pub fn verify(&self, bytes: &[u8], signer: &PublicKey) -> Result<(), AuthnSignatureError> { - self.gc(); - - if bytes.len() != 96 { - return Err(AuthnSignatureError::InvalidLength(bytes.len())); - } - - let signature_bytes: SignatureBytes = bytes[0..64] - .try_into() - .expect("validate token length on instantiating"); - let signature = Signature::from(signature_bytes); - - let token_hash: [u8; 32] = bytes[64..].try_into().expect("should not be reachable"); - - let now = Timestamp::now().into_inner(); - let past = now - TIME_INTERVAL; - let future = now + TIME_INTERVAL; - - let result = verify_at(now, self, &signature, signer, &token_hash); - - match result { - Ok(_) => return Ok(()), - Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), - _ => {} - } - - let result = verify_at(past, self, &signature, signer, &token_hash); - - match result { - Ok(_) => return Ok(()), - Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), - _ => {} - } - - verify_at(future, self, &signature, signer, &token_hash) - } - - // === Private Methods === - - /// Remove all tokens older than two time intervals in the past. - fn gc(&self) { - let threshold = ((Timestamp::now().into_inner() / TIME_INTERVAL) - 2).to_be_bytes(); - - let mut inner = self.inner.lock().unwrap(); - - match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) { - Ok(index) | Err(index) => { - inner.drain(0..index); - } - } - } -} - -fn verify_at( - time: u64, - verifier: &AuthnVerifier, - signature: &Signature, - signer: &PublicKey, - token_hash: &[u8; 32], -) -> Result<(), AuthnSignatureError> { - let time_step = time / TIME_INTERVAL; - let time_step_bytes = time_step.to_be_bytes(); - - let result = signer.verify( - &signable(&time_step_bytes, signer, &verifier.audience, token_hash), - signature, - ); - - if result.is_ok() { - let mut inner = verifier.inner.lock().unwrap(); - - let mut candidate = [0_u8; 40]; - candidate[..8].copy_from_slice(&time_step_bytes); - candidate[8..].copy_from_slice(token_hash); - - match inner.binary_search_by(|element| element.cmp(&candidate)) { - Ok(index) | Err(index) => { - inner.insert(index, candidate); - } - }; - - return Ok(()); - } - - Err(AuthnSignatureError::InvalidSignature) -} - -fn signable( - time_step_bytes: &[u8; 8], - signer: &PublicKey, - audience: &PublicKey, - token_hash: &[u8; 32], -) -> [u8; 115] { - let mut arr = [0; 115]; - - arr[..11].copy_from_slice(crate::namespaces::PUBKY_AUTHN); - arr[19..51].copy_from_slice(signer.as_bytes()); - arr[11..19].copy_from_slice(time_step_bytes); - arr[51..83].copy_from_slice(audience.as_bytes()); - arr[83..].copy_from_slice(token_hash); - - arr -} - -#[derive(thiserror::Error, Debug)] -pub enum AuthnSignatureError { - #[error("AuthnSignature should be 96 bytes long, got {0} bytes instead")] - InvalidLength(usize), - - #[error("Invalid signature")] - InvalidSignature, - - #[error("Authn signature already used")] - AlreadyUsed, -} - #[cfg(test)] mod tests { use crate::{auth::TIMESTAMP_WINDOW, crypto::Keypair, timestamp::Timestamp}; @@ -427,4 +266,23 @@ mod tests { assert_eq!(result, Err(Error::Expired)); } + + #[test] + fn already_used() { + let signer = Keypair::random(); + let audience = Keypair::random().public_key(); + let scopes = vec!["*:*".to_string()]; + + let verifier = AuthVerifier::new(audience.clone()); + + let token = AuthToken::sign(&signer, &audience, scopes.clone()); + + let serialized = &token.serialize(); + + verifier.verify(serialized).unwrap(); + + assert_eq!(token.scopes, scopes); + + assert_eq!(verifier.verify(serialized), Err(Error::AlreadyUsed)); + } } diff --git a/pubky-homeserver/src/error.rs b/pubky-homeserver/src/error.rs index 5fad2f0..b6e5a14 100644 --- a/pubky-homeserver/src/error.rs +++ b/pubky-homeserver/src/error.rs @@ -5,7 +5,6 @@ use axum::{ http::StatusCode, response::IntoResponse, }; -use pubky_common::auth::AuthnSignatureError; pub type Result = core::result::Result; @@ -71,8 +70,8 @@ impl From for Error { // === Pubky specific errors === -impl From for Error { - fn from(error: AuthnSignatureError) -> Self { +impl From for Error { + fn from(error: pubky_common::auth::Error) -> Self { Self::new(StatusCode::BAD_REQUEST, Some(error)) } } diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 35615fd..085a5c5 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -18,9 +18,9 @@ mod root; fn base(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) - .route("/:pubky", put(auth::signup)) + .route("/signup", post(auth::signup)) + .route("/session", post(auth::signin)) .route("/:pubky/session", get(auth::session)) - .route("/:pubky/session", post(auth::signin)) .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*path", put(public::put)) .route("/:pubky/*path", get(public::get)) diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 72246f9..5f035e2 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -25,13 +25,12 @@ pub async fn signup( State(state): State, user_agent: Option>, cookies: Cookies, - pubky: Pubky, uri: Uri, body: Bytes, ) -> Result { // TODO: Verify invitation link. // TODO: add errors in case of already axisting user. - signin(State(state), user_agent, cookies, pubky, uri, body).await + signin(State(state), user_agent, cookies, uri, body).await } pub async fn session( @@ -90,13 +89,12 @@ pub async fn signin( State(state): State, user_agent: Option>, cookies: Cookies, - pubky: Pubky, uri: Uri, body: Bytes, ) -> Result { - let public_key = pubky.public_key(); + let token = state.verifier.verify(&body)?; - state.verifier.verify(&body, public_key)?; + let public_key = token.subject(); let mut wtxn = state.db.env.write_txn()?; let users: UsersTable = state diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index cdc352c..4ef528e 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,7 +1,7 @@ use std::{future::IntoFuture, net::SocketAddr}; use anyhow::{Error, Result}; -use pubky_common::auth::AuthnVerifier; +use pubky_common::auth::AuthVerifier; use tokio::{net::TcpListener, signal, task::JoinSet}; use tracing::{debug, info, warn}; @@ -21,7 +21,7 @@ pub struct Homeserver { #[derive(Clone, Debug)] pub(crate) struct AppState { - pub verifier: AuthnVerifier, + pub verifier: AuthVerifier, pub db: DB, pub pkarr_client: PkarrClientAsync, } @@ -46,7 +46,7 @@ impl Homeserver { .as_async(); let state = AppState { - verifier: AuthnVerifier::new(public_key.clone()), + verifier: AuthVerifier::new(public_key.clone()), db, pkarr_client: pkarr_client.clone(), }; diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index d4a6436..072461c 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,7 +1,7 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; -use pubky_common::{auth::AuthnSignature, session::Session}; +use pubky_common::{auth::AuthToken, session::Session}; use crate::{error::Result, PubkyClient}; @@ -19,21 +19,17 @@ impl PubkyClient { ) -> Result<()> { let homeserver = homeserver.to_string(); - let public_key = &keypair.public_key(); - let Endpoint { public_key: audience, mut url, } = self.resolve_endpoint(&homeserver).await?; - url.set_path(&format!("/{}", public_key)); + url.set_path(&format!("/signup")); - let body = AuthnSignature::generate(keypair, &audience) - .as_bytes() - .to_owned(); + let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); let response = self - .request(Method::PUT, url.clone()) + .request(Method::POST, url.clone()) .body(body) .send() .await?; @@ -91,11 +87,9 @@ impl PubkyClient { mut url, } = self.resolve_pubky_homeserver(&pubky).await?; - url.set_path(&format!("/{}/session", &pubky)); + url.set_path(&format!("/session")); - let body = AuthnSignature::generate(keypair, &audience) - .as_bytes() - .to_owned(); + let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); let response = self.request(Method::POST, url).body(body).send().await?; From 892d5b2d9ac6e789d624dd4d6d383d3bf98b7796 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 28 Aug 2024 20:34:28 +0300 Subject: [PATCH 090/125] fix(pubky): resolve_endpoint break loop if packet not resolved --- pubky/src/shared/pkarr.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index e624a2a..fa3f866 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -132,6 +132,8 @@ impl PubkyClient { continue; }; } + } else { + break; } } From 8118e94430c7dd85d58648ae7392037e7fa0d5ef Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 28 Aug 2024 20:39:32 +0300 Subject: [PATCH 091/125] fix(pubky): return error if resolve_endpoint couldn't find non Pkarr domain --- pubky/src/shared/pkarr.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index fa3f866..7a29e2e 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -137,6 +137,10 @@ impl PubkyClient { } } + if PublicKey::try_from(origin.as_str()).is_ok() { + return Err(Error::ResolveEndpoint(original_target.into())); + } + if let Some(public_key) = endpoint_public_key { let url = Url::parse(&format!( "{}://{}", From 16dfc096874b3ba8c8de88b61fc311faef899f4c Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 29 Aug 2024 11:48:21 +0300 Subject: [PATCH 092/125] feat(homeserver): store and return session capabilities --- pubky-common/src/auth.rs | 41 +++--- pubky-common/src/capabilities.rs | 170 ++++++++++++++++++++++++ pubky-common/src/lib.rs | 1 + pubky-common/src/session.rs | 27 +++- pubky-homeserver/src/database/tables.rs | 13 ++ pubky-homeserver/src/routes/auth.rs | 27 ++-- pubky/src/shared/auth.rs | 16 +-- 7 files changed, 247 insertions(+), 48 deletions(-) create mode 100644 pubky-common/src/capabilities.rs diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 8f95fbc..71e69a5 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex}; use serde::{Deserialize, Serialize}; use crate::{ + capabilities::Capability, crypto::{Keypair, PublicKey, Signature}, timestamp::Timestamp, }; @@ -30,12 +31,12 @@ pub struct AuthToken { subject: PublicKey, /// The Pubky of the party verifying the [AuthToken], for example a web server. audience: PublicKey, - // Variable length scopes - scopes: Vec, + // Variable length capabilities + capabilities: Vec, } impl AuthToken { - pub fn sign(signer: &Keypair, audience: &PublicKey, scopes: Vec) -> Self { + pub fn sign(signer: &Keypair, audience: &PublicKey, capabilities: Vec) -> Self { let timestamp = Timestamp::now(); let mut token = Self { @@ -43,7 +44,7 @@ impl AuthToken { subject: signer.public_key(), audience: audience.to_owned(), timestamp, - scopes, + capabilities, signature: Signature::from_bytes(&[0; 64]), }; @@ -54,6 +55,10 @@ impl AuthToken { token } + pub fn capabilities(&self) -> &[Capability] { + &self.capabilities + } + /// Authenticate signer to an audience directly with [] capailities. /// /// @@ -150,7 +155,7 @@ impl AuthVerifier { match seen.binary_search_by(|element| element.cmp(&id)) { Ok(_) => Err(Error::AlreadyUsed), Err(index) => { - seen.insert(index, id.into()); + seen.insert(index, id); Ok(token) } } @@ -192,7 +197,9 @@ pub enum Error { #[cfg(test)] mod tests { - use crate::{auth::TIMESTAMP_WINDOW, crypto::Keypair, timestamp::Timestamp}; + use crate::{ + auth::TIMESTAMP_WINDOW, capabilities::Capability, crypto::Keypair, timestamp::Timestamp, + }; use super::{AuthToken, AuthVerifier, Error}; @@ -200,9 +207,9 @@ mod tests { fn v0_id_signable() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; - let token = AuthToken::sign(&signer, &audience, scopes.clone()); + let token = AuthToken::sign(&signer, &audience, capabilities.clone()); let serialized = &token.serialize(); @@ -221,24 +228,24 @@ mod tests { fn sign_verify() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; let verifier = AuthVerifier::new(audience.clone()); - let token = AuthToken::sign(&signer, &audience, scopes.clone()); + let token = AuthToken::sign(&signer, &audience, capabilities.clone()); let serialized = &token.serialize(); verifier.verify(serialized).unwrap(); - assert_eq!(token.scopes, scopes); + assert_eq!(token.capabilities, capabilities); } #[test] fn expired() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; let verifier = AuthVerifier::new(audience.clone()); @@ -247,7 +254,7 @@ mod tests { let mut signable = vec![]; signable.extend_from_slice(signer.public_key().as_bytes()); signable.extend_from_slice(audience.as_bytes()); - signable.extend_from_slice(&postcard::to_allocvec(&scopes).unwrap()); + signable.extend_from_slice(&postcard::to_allocvec(&capabilities).unwrap()); let signature = signer.sign(&signable); @@ -257,7 +264,7 @@ mod tests { audience, timestamp, signature, - scopes, + capabilities, }; let serialized = token.serialize(); @@ -271,17 +278,17 @@ mod tests { fn already_used() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; let verifier = AuthVerifier::new(audience.clone()); - let token = AuthToken::sign(&signer, &audience, scopes.clone()); + let token = AuthToken::sign(&signer, &audience, capabilities.clone()); let serialized = &token.serialize(); verifier.verify(serialized).unwrap(); - assert_eq!(token.scopes, scopes); + assert_eq!(token.capabilities, capabilities); assert_eq!(verifier.verify(serialized), Err(Error::AlreadyUsed)); } diff --git a/pubky-common/src/capabilities.rs b/pubky-common/src/capabilities.rs new file mode 100644 index 0000000..2a46cde --- /dev/null +++ b/pubky-common/src/capabilities.rs @@ -0,0 +1,170 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +const PUBKY_CAP_PREFIX: &str = "pk!"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Capability { + /// Pubky Homeserver's capabilities + Pubky(PubkyCap), + Unknown(String), +} + +impl Capability { + /// Create a [PubkyCap] at the root path `/` with all the available [PubkyAbility] + pub fn pubky_root() -> Self { + Capability::Pubky(PubkyCap { + path: "/".to_string(), + abilities: vec![PubkyAbility::Read, PubkyAbility::Write], + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PubkyCap { + pub path: String, + pub abilities: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PubkyAbility { + /// Can read the resource at the specified path (GET requests). + Read, + /// Can write to the resource at the specified path (PUT/POST/DELETE requests). + Write, +} + +impl From<&PubkyAbility> for char { + fn from(value: &PubkyAbility) -> Self { + match value { + PubkyAbility::Read => 'r', + PubkyAbility::Write => 'w', + } + } +} + +impl TryFrom for PubkyAbility { + type Error = Error; + + fn try_from(value: char) -> Result { + match value { + 'r' => Ok(Self::Read), + 'w' => Ok(Self::Write), + _ => Err(Error::InvalidPubkyAbility), + } + } +} + +impl TryFrom for Capability { + type Error = Error; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl Display for Capability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pubky(cap) => write!( + f, + "{}{}:{}", + PUBKY_CAP_PREFIX, + cap.path, + cap.abilities.iter().map(char::from).collect::() + ), + Self::Unknown(string) => write!(f, "{string}"), + } + } +} + +impl TryFrom<&str> for Capability { + type Error = Error; + + fn try_from(value: &str) -> Result { + if value.starts_with(PUBKY_CAP_PREFIX) { + let mut rsplit = value.rsplit(':'); + + let mut abilities = Vec::new(); + + for char in rsplit + .next() + .ok_or(Error::MissingField("abilities"))? + .chars() + { + let ability = PubkyAbility::try_from(char)?; + + match abilities.binary_search_by(|element| char::from(element).cmp(&char)) { + Ok(_) => {} + Err(index) => { + abilities.insert(index, ability); + } + } + } + + let path = rsplit.next().ok_or(Error::MissingField("path"))?[PUBKY_CAP_PREFIX.len()..] + .to_string(); + + if !path.starts_with('/') { + return Err(Error::InvalidPath); + } + + return Ok(Capability::Pubky(PubkyCap { path, abilities })); + } + + Ok(Capability::Unknown(value.to_string())) + } +} + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum Error { + #[error("PubkyCap: Missing field {0}")] + MissingField(&'static str), + #[error("PubkyCap: InvalidPath does not start with `/`")] + InvalidPath, + #[error("Invalid PubkyAbility")] + InvalidPubkyAbility, +} + +impl Serialize for Capability { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let string = self.to_string(); + + string.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Capability { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string: String = Deserialize::deserialize(deserializer)?; + + string.try_into().map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pubky_caps() { + let cap = Capability::Pubky(PubkyCap { + path: "/pub/pubky.app/".to_string(), + abilities: vec![PubkyAbility::Read, PubkyAbility::Write], + }); + + // Read and write withing directory `/pub/pubky.app/`. + let expected_string = "pk!/pub/pubky.app/:rw"; + + assert_eq!(cap.to_string(), expected_string); + + assert_eq!(Capability::try_from(expected_string), Ok(cap)) + } +} diff --git a/pubky-common/src/lib.rs b/pubky-common/src/lib.rs index cedc227..5234c51 100644 --- a/pubky-common/src/lib.rs +++ b/pubky-common/src/lib.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod capabilities; pub mod crypto; pub mod namespaces; pub mod session; diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 5a35e14..83be3a7 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; extern crate alloc; use alloc::vec::Vec; -use crate::timestamp::Timestamp; +use crate::{auth::AuthToken, capabilities::Capability, timestamp::Timestamp}; // TODO: add IP address? // TODO: use https://crates.io/crates/user-agent-parser to parse the session @@ -16,14 +16,23 @@ pub struct Session { /// User specified name, defaults to the user-agent. pub name: String, pub user_agent: String, + pub capabilities: Vec, } impl Session { - pub fn new() -> Self { - Self { + pub fn new(token: &AuthToken, user_agent: Option) -> Self { + let mut session = Self { created_at: Timestamp::now().into_inner(), ..Default::default() + }; + + session.set_capabilities(token.capabilities().to_vec()); + + if let Some(user_agent) = user_agent { + session.set_user_agent(user_agent); } + + session } // === Setters === @@ -38,6 +47,12 @@ impl Session { self } + pub fn set_capabilities(&mut self, capabilities: Vec) -> &mut Self { + self.capabilities = capabilities; + + self + } + // === Public Methods === pub fn serialize(&self) -> Vec { @@ -71,12 +86,16 @@ mod tests { fn serialize() { let session = Session { user_agent: "foo".to_string(), + capabilities: vec![Capability::pubky_root()], ..Default::default() }; let serialized = session.serialize(); - assert_eq!(serialized, [0, 0, 0, 3, 102, 111, 111,]); + assert_eq!( + serialized, + [0, 0, 0, 3, 102, 111, 111, 1, 7, 112, 107, 33, 47, 58, 114, 119] + ); let deseiralized = Session::deserialize(&serialized).unwrap(); diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index a019fbe..0f5f287 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -8,10 +8,17 @@ use heed::{Env, RwTxn}; use blobs::{BlobsTable, BLOBS_TABLE}; use entries::{EntriesTable, ENTRIES_TABLE}; +use self::{ + sessions::{SessionsTable, SESSIONS_TABLE}, + users::{UsersTable, USERS_TABLE}, +}; + pub const TABLES_COUNT: u32 = 4; #[derive(Debug, Clone)] pub struct Tables { + pub users: UsersTable, + pub sessions: SessionsTable, pub blobs: BlobsTable, pub entries: EntriesTable, } @@ -19,6 +26,12 @@ pub struct Tables { impl Tables { pub fn new(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result { Ok(Self { + users: env + .open_database(wtxn, Some(USERS_TABLE))? + .expect("Users table already created"), + sessions: env + .open_database(wtxn, Some(SESSIONS_TABLE))? + .expect("Sessions table already created"), blobs: env .open_database(wtxn, Some(BLOBS_TABLE))? .expect("Blobs table already created"), diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 5f035e2..9bfe5f8 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -13,7 +13,7 @@ use pubky_common::{crypto::random_bytes, session::Session, timestamp::Timestamp} use crate::{ database::tables::{ sessions::{SessionsTable, SESSIONS_TABLE}, - users::{User, UsersTable, USERS_TABLE}, + users::User, }, error::{Error, Result}, extractors::Pubky, @@ -97,12 +97,8 @@ pub async fn signin( let public_key = token.subject(); let mut wtxn = state.db.env.write_txn()?; - let users: UsersTable = state - .db - .env - .open_database(&wtxn, Some(USERS_TABLE))? - .expect("Users table already created"); + let users = state.db.tables.users; if let Some(existing) = users.get(&wtxn, public_key)? { users.put(&mut wtxn, public_key, &existing)?; } else { @@ -117,21 +113,14 @@ pub async fn signin( let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); - let sessions: SessionsTable = state - .db - .env - .open_database(&wtxn, Some(SESSIONS_TABLE))? - .expect("Sessions table already created"); - - let mut session = Session::new(); - - if let Some(user_agent) = user_agent { - session.set_user_agent(user_agent.to_string()); - } - - sessions.put(&mut wtxn, &session_secret, &session.serialize())?; + state.db.tables.sessions.put( + &mut wtxn, + &session_secret, + &Session::new(&token, user_agent.map(|ua| ua.to_string())).serialize(), + )?; let mut cookie = Cookie::new(public_key.to_string(), session_secret); + cookie.set_path("/"); if *uri.scheme().unwrap_or(&Scheme::HTTP) == Scheme::HTTPS { cookie.set_secure(true); diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 072461c..6cbb65d 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,7 +1,7 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; -use pubky_common::{auth::AuthToken, session::Session}; +use pubky_common::{auth::AuthToken, capabilities::Capability, session::Session}; use crate::{error::Result, PubkyClient}; @@ -24,9 +24,9 @@ impl PubkyClient { mut url, } = self.resolve_endpoint(&homeserver).await?; - url.set_path(&format!("/signup")); + url.set_path("/signup"); - let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); + let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize(); let response = self .request(Method::POST, url.clone()) @@ -87,9 +87,9 @@ impl PubkyClient { mut url, } = self.resolve_pubky_homeserver(&pubky).await?; - url.set_path(&format!("/session")); + url.set_path("/session"); - let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); + let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize(); let response = self.request(Method::POST, url).body(body).send().await?; @@ -105,7 +105,7 @@ mod tests { use crate::*; use pkarr::{mainline::Testnet, Keypair}; - use pubky_common::session::Session; + use pubky_common::capabilities::Capability; use pubky_homeserver::Homeserver; #[tokio::test] @@ -125,7 +125,7 @@ mod tests { .unwrap() .unwrap(); - assert_eq!(session, Session { ..session.clone() }); + assert!(session.capabilities.contains(&Capability::pubky_root())); client.signout(&keypair.public_key()).await.unwrap(); @@ -144,7 +144,7 @@ mod tests { .unwrap() .unwrap(); - assert_eq!(session, Session { ..session.clone() }); + assert!(session.capabilities.contains(&Capability::pubky_root())); } } } From 51c3e444b0c016986a438b27a1f3006ce79fcc85 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 29 Aug 2024 19:05:36 +0300 Subject: [PATCH 093/125] examples(authz): start 3rd party app frontend --- Cargo.lock | 4 + Cargo.toml | 2 +- examples/authz/3rd-party-app/.eslintignore | 5 + examples/authz/3rd-party-app/.eslintrc.json | 26 + .../3rd-party-app/.github/dependabot.yml | 6 + examples/authz/3rd-party-app/.gitignore | 669 +++ examples/authz/3rd-party-app/.prettierignore | 2 + examples/authz/3rd-party-app/.prettierrc | 8 + examples/authz/3rd-party-app/README.md | 132 + examples/authz/3rd-party-app/index.html | 15 + .../authz/3rd-party-app/package-lock.json | 4754 +++++++++++++++++ examples/authz/3rd-party-app/package.json | 33 + .../authz/3rd-party-app/postcss.config.cjs | 13 + .../authz/3rd-party-app/public/screenshot.png | Bin 0 -> 70373 bytes examples/authz/3rd-party-app/public/vite.svg | 1 + examples/authz/3rd-party-app/src/js/main.js | 69 + .../authz/3rd-party-app/src/styles/style.css | 134 + examples/authz/3rd-party-app/vite.config.js | 13 + 18 files changed, 5885 insertions(+), 1 deletion(-) create mode 100644 examples/authz/3rd-party-app/.eslintignore create mode 100644 examples/authz/3rd-party-app/.eslintrc.json create mode 100644 examples/authz/3rd-party-app/.github/dependabot.yml create mode 100644 examples/authz/3rd-party-app/.gitignore create mode 100644 examples/authz/3rd-party-app/.prettierignore create mode 100644 examples/authz/3rd-party-app/.prettierrc create mode 100644 examples/authz/3rd-party-app/README.md create mode 100644 examples/authz/3rd-party-app/index.html create mode 100644 examples/authz/3rd-party-app/package-lock.json create mode 100644 examples/authz/3rd-party-app/package.json create mode 100644 examples/authz/3rd-party-app/postcss.config.cjs create mode 100644 examples/authz/3rd-party-app/public/screenshot.png create mode 100644 examples/authz/3rd-party-app/public/vite.svg create mode 100644 examples/authz/3rd-party-app/src/js/main.js create mode 100644 examples/authz/3rd-party-app/src/styles/style.css create mode 100644 examples/authz/3rd-party-app/vite.config.js diff --git a/Cargo.lock b/Cargo.lock index df600ac..b3862bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,10 @@ dependencies = [ "critical-section", ] +[[package]] +name = "authenticator" +version = "0.1.0" + [[package]] name = "autocfg" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 4fcef4e..d1fbeb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "pubky","pubky-*"] +members = ["pubky","pubky-*"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/examples/authz/3rd-party-app/.eslintignore b/examples/authz/3rd-party-app/.eslintignore new file mode 100644 index 0000000..093420c --- /dev/null +++ b/examples/authz/3rd-party-app/.eslintignore @@ -0,0 +1,5 @@ +.vscode +dist +node_modules +vite.config.ts +postcss.config.js \ No newline at end of file diff --git a/examples/authz/3rd-party-app/.eslintrc.json b/examples/authz/3rd-party-app/.eslintrc.json new file mode 100644 index 0000000..2dc6d48 --- /dev/null +++ b/examples/authz/3rd-party-app/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "root": true, + "env": { + "browser": true, + "es2023": true, + "node": true + }, + "extends": [ + "plugin:import/recommended", + "eslint-config-prettier" + ], + "plugins": ["import"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "settings": { + "import/resolver": { + "node": { + "extensions": [".js"], + "path": ["src"], + "moduleDirectory": ["node_modules"] + } + } + } +} diff --git a/examples/authz/3rd-party-app/.github/dependabot.yml b/examples/authz/3rd-party-app/.github/dependabot.yml new file mode 100644 index 0000000..7b2ee3a --- /dev/null +++ b/examples/authz/3rd-party-app/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily diff --git a/examples/authz/3rd-party-app/.gitignore b/examples/authz/3rd-party-app/.gitignore new file mode 100644 index 0000000..e6552dd --- /dev/null +++ b/examples/authz/3rd-party-app/.gitignore @@ -0,0 +1,669 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,intellij,node +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,intellij,node + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +*.code-workspace + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,intellij,node diff --git a/examples/authz/3rd-party-app/.prettierignore b/examples/authz/3rd-party-app/.prettierignore new file mode 100644 index 0000000..db4c6d9 --- /dev/null +++ b/examples/authz/3rd-party-app/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/examples/authz/3rd-party-app/.prettierrc b/examples/authz/3rd-party-app/.prettierrc new file mode 100644 index 0000000..986cbad --- /dev/null +++ b/examples/authz/3rd-party-app/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 80, + "tabWidth": 4, + "singleQuote": true, + "trailingComma": "all", + "useTabs": false, + "semi": true +} diff --git a/examples/authz/3rd-party-app/README.md b/examples/authz/3rd-party-app/README.md new file mode 100644 index 0000000..b0c5b00 --- /dev/null +++ b/examples/authz/3rd-party-app/README.md @@ -0,0 +1,132 @@ +# Vite Vanilla JS Template + +![screenshot](/public/screenshot.png) + +Initially built for personal use, I created this template for starting a new project with Vite.js and Vanilla Javascript. It is already set up with standard development tools like ESLint and Prettier for easy code formatting and linting, with Vite for a robust, modern build process. + +## Dependencies + +This template uses the following dependencies: + +- **[Vite](https://vitejs.dev/):** A next-generation frontend build tool that offers a fast dev server and optimized builds. +- **[ESLint](https://eslint.org/):** An open-source JavaScript linting utility that helps maintain a consistent code style. +- **[Prettier](https://prettier.io/):** An opinionated code formatter that enforces a consistent style across your project. +- **[eslint-config-airbnb-base](https://www.npmjs.com/package/eslint-config-airbnb-base) and [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier):** ESLint configurations adhering to Airbnb's base JS style guide and disabling stylistic rules that might conflict with Prettier. +- **[eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import) and [eslint-plugin-prettier](https://www.npmjs.com/package/eslint-plugin-prettier):** ESLint plugins that enforce ES2015+ import/export syntax and integrate Prettier with ESLint. +- **[autoprefixer](https://www.npmjs.com/package/autoprefixer) and [postcss](https://postcss.org/):** Autoprefixer automatically adds vendor prefixes to CSS, while PostCSS provides a way to transform CSS with JavaScript. +- **[cssnano](https://cssnano.github.io/cssnano/):** A tool that helps to compress and optimize CSS files. +- **[postcss-nesting](https://www.npmjs.com/package/postcss-nesting):** A PostCSS plugin that allows you to use modern CSS Nesting in your stylesheets. +- **[vite-plugin-eslint](https://www.npmjs.com/package/vite-plugin-eslint):** Integrates ESLint into the Vite build process for on-the-fly linting. +- **[the-new-css-reset](https://elad2412.github.io/the-new-css-reset/):** A modern, CSS reset for your styles. + +## Cloning + +1. To start using this template, clone the repository with this command: + +```bash +git clone https://github.com/Barata-Ribeiro/vite-vanilla-js-template.git +``` + +2. Then proceed to the folder and install dependencies: + +```bash +cd vite-vanilla-js-template +npm install +``` + +**or** + +```bash +npm install -g degit # if you don't have degit installed... + +#degit documentation at: https://github.com/Rich-Harris/degit +``` + +1. Use 'degit' to create a folder project using this template: + +```bash +degit Barata-Ribeiro/vite-vanilla-js-template your-project-name +``` + +2. Then proceed to the folder and install dependencies: + +```bash +cd your-project-name +npm install +``` + + +## Post-Cloning Steps + +After cloning the template, make sure to clean up and update the following: + +1. Remove the .git directory and run `git init` to clean the commit history. +2. Clean up the README.md file. +3. Adapt the LICENSE file to your project. +4. Delete `public/vite.svg`, `public/screenshot`, `src/assets/images/javascript.svg`, and `src/assets/images/vite.svg`. +5. Delete the content from `src/styles/style.css`. +6. In the `src/js/main.js` file, leave only these import statements: `import "../../styles/style.css";` and `import "the-new-css-reset/css/reset.css";`. +7. Adapt the `package.json` file with your project's own information. +8. Delete the .github folder. + +## Scripts + +Use the following scripts for your development workflow: + +```bash +# Start the development server +npm run dev + +# Checks your code for any linting errors +npm run lint + +# Tries to automatically fix any linting errors present in your code +npm run lint:fix + +# Formats your code in a consistent, predefined style using Prettier +npm run format + +# Build for production +npm run build + +# Preview the build +npm run preview + +# Build and preview the project +npm run buildpreview +``` + +## Folder Structure + +This is the structure of the project: + +```plaintext +/ +├── .github # Github actions and workflows +├── node_modules # Node.js dependencies for the project. +├── public # Public assets and resources +├── src # Source code +│ ├── assets # General assets for your project +│ │ ├── images # Store your images here +│ ├── js # Javascript files of your project +│ ├── styles # CSS styles for your project +├── .editorconfig # Configuration for the EditorConfig plugin +├── .eslintignore # Files to be ignored by ESLint +├── .eslintrc.json # Configuration for ESLint +├── .gitignore # Files and folders to be ignored by Git +├── .prettierignore # Files to be ignored by Prettier +├── .prettierrc # Configuration for Prettier +├── index.html # The HTML file for your project +├── LICENSE # The license for your project +├── package-lock.json # Lockfile for your project's dependencies +├── package.json # Defines your project and its dependencies +├── postcss.config.cjs # Configuration for PostCSS +├── README.md # This file +├── vite.config.js # Configuration for Vite +``` + +## License + +This template was created under the [MIT License](LICENSE.md). + +**Happy coding!** 👨‍💻 diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html new file mode 100644 index 0000000..79559cf --- /dev/null +++ b/examples/authz/3rd-party-app/index.html @@ -0,0 +1,15 @@ + + + + + + + Vite.js Vanilla Template + + + +
+
+ + + diff --git a/examples/authz/3rd-party-app/package-lock.json b/examples/authz/3rd-party-app/package-lock.json new file mode 100644 index 0000000..6058b0c --- /dev/null +++ b/examples/authz/3rd-party-app/package-lock.json @@ -0,0 +1,4754 @@ +{ + "name": "pubky-auth-3rd-party", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pubky-auth-3rd-party", + "dependencies": { + "qrcode": "^1.5.4", + "the-new-css-reset": "^1.11.3" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "cssnano": "^7.0.5", + "eslint": "^8.57.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.2.1", + "postcss": "^8.4.41", + "postcss-nesting": "^13.0.0", + "prettier": "^3.3.3", + "vite": "5.4.2", + "vite-plugin-eslint": "^1.8.1" + } + }, + "node_modules/@csstools/selector-resolve-nested": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-2.0.0.tgz", + "integrity": "sha512-oklSrRvOxNeeOW1yARd4WNCs/D09cQjunGZUgSq6vM8GpzFswN+8rBZyJA29YFZhOTQ6GFzxgLDNtVbt9wPZMA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.1.0" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz", + "integrity": "sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz", + "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz", + "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", + "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz", + "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz", + "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz", + "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz", + "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz", + "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz", + "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz", + "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz", + "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", + "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz", + "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz", + "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz", + "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz", + "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001649", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz", + "integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.5.tgz", + "integrity": "sha512-Aq0vqBLtpTT5Yxj+hLlLfNPFuRQCDIjx5JQAhhaedQKLNDvDGeVziF24PS+S1f0Z5KCxWvw0QVI3VNHNBITxVQ==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^7.0.5", + "lilconfig": "^3.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.5.tgz", + "integrity": "sha512-Jbzja0xaKwc5JzxPQoc+fotKpYtWEu4wQLMQe29CM0FjjdRjA4omvbGHl2DTGgARKxSTpPssBsok+ixv8uTBqw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.0", + "postcss-calc": "^10.0.1", + "postcss-colormin": "^7.0.2", + "postcss-convert-values": "^7.0.3", + "postcss-discard-comments": "^7.0.2", + "postcss-discard-duplicates": "^7.0.1", + "postcss-discard-empty": "^7.0.0", + "postcss-discard-overridden": "^7.0.0", + "postcss-merge-longhand": "^7.0.3", + "postcss-merge-rules": "^7.0.3", + "postcss-minify-font-values": "^7.0.0", + "postcss-minify-gradients": "^7.0.0", + "postcss-minify-params": "^7.0.2", + "postcss-minify-selectors": "^7.0.3", + "postcss-normalize-charset": "^7.0.0", + "postcss-normalize-display-values": "^7.0.0", + "postcss-normalize-positions": "^7.0.0", + "postcss-normalize-repeat-style": "^7.0.0", + "postcss-normalize-string": "^7.0.0", + "postcss-normalize-timing-functions": "^7.0.0", + "postcss-normalize-unicode": "^7.0.2", + "postcss-normalize-url": "^7.0.0", + "postcss-normalize-whitespace": "^7.0.0", + "postcss-ordered-values": "^7.0.1", + "postcss-reduce-initial": "^7.0.2", + "postcss-reduce-transforms": "^7.0.0", + "postcss-svgo": "^7.0.1", + "postcss-unique-selectors": "^7.0.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", + "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", + "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.1.tgz", + "integrity": "sha512-pp1Z3FxtxA+xHAoWXcOXgnBN1WPu4ZiJ5LWGjKyf9MMreagAsaTUtnqFK1y1sHhyJddAkYTPu6XSuLgb3oYCjw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/postcss-colormin": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", + "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.3.tgz", + "integrity": "sha512-yJhocjCs2SQer0uZ9lXTMOwDowbxvhwFVrZeS6NPEij/XXthl73ggUmfwVvJM+Vaj5gtCKJV1jiUu4IhAUkX/Q==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-comments": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.2.tgz", + "integrity": "sha512-/Hje9Ls1IYcB9duELO/AyDUJI6aQVY3h5Rj1ziXgaLYCTi1iVBLnjg/TS0D6NszR/kDG6I86OwLmAYe+bvJjiQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", + "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", + "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", + "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.3.tgz", + "integrity": "sha512-8waYomFxshdv6M9Em3QRM9MettRLDRcH2JQi2l0Z1KlYD/vhal3gbkeSES0NuACXOlZBB0V/B0AseHZaklzWOA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.3.tgz", + "integrity": "sha512-2eSas2p3voPxNfdI5sQrvIkMaeUHpVc3EezgVs18hz/wRTQAC9U99tp9j3W5Jx9/L3qHkEDvizEx/LdnmumIvQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.0", + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", + "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", + "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", + "dev": true, + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", + "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.3.tgz", + "integrity": "sha512-SxTgUQSgBk6wEqzQZKEv1xQYIp9UBju6no9q+npohzSdhuSICQdkqmD1UMKkZWItS3olJSJMDDEY9WOJ5oGJew==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.0.tgz", + "integrity": "sha512-TCGQOizyqvEkdeTPM+t6NYwJ3EJszYE/8t8ILxw/YoeUvz2rz7aM8XTAmBWh9/DJjfaaabL88fWrsVHSPF2zgA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-resolve-nested": "^2.0.0", + "@csstools/selector-specificity": "^4.0.0", + "postcss-selector-parser": "^6.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", + "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", + "dev": true, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", + "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", + "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", + "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", + "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", + "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", + "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", + "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", + "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", + "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", + "dev": true, + "dependencies": { + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", + "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", + "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", + "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", + "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.3.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.2.tgz", + "integrity": "sha512-CjSam+7Vf8cflJQsHrMS0P2hmy9u0+n/P001kb5eAszLmhjMqrt/i5AqQuNFihhViwDvEAezqTmXqaYXL2ugMw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", + "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.0", + "@rollup/rollup-android-arm64": "4.21.0", + "@rollup/rollup-darwin-arm64": "4.21.0", + "@rollup/rollup-darwin-x64": "4.21.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", + "@rollup/rollup-linux-arm-musleabihf": "4.21.0", + "@rollup/rollup-linux-arm64-gnu": "4.21.0", + "@rollup/rollup-linux-arm64-musl": "4.21.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", + "@rollup/rollup-linux-riscv64-gnu": "4.21.0", + "@rollup/rollup-linux-s390x-gnu": "4.21.0", + "@rollup/rollup-linux-x64-gnu": "4.21.0", + "@rollup/rollup-linux-x64-musl": "4.21.0", + "@rollup/rollup-win32-arm64-msvc": "4.21.0", + "@rollup/rollup-win32-ia32-msvc": "4.21.0", + "@rollup/rollup-win32-x64-msvc": "4.21.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylehacks": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.3.tgz", + "integrity": "sha512-4DqtecvI/Nd+2BCvW9YEF6lhBN5UM50IJ1R3rnEAhBwbCKf4VehRf+uqvnVArnBayjYD/WtT3g0G/HSRxWfTRg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3", + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/the-new-css-reset": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/the-new-css-reset/-/the-new-css-reset-1.11.3.tgz", + "integrity": "sha512-61SB81vu9foUyEIqoU1CeqxrdlsVjJojj/CBXoG8BdvlKFsllB0Rza63DblnRqH+3uttPj3FGWo7+c9nu7MT+A==" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-eslint": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz", + "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^4.2.1", + "@types/eslint": "^8.4.5", + "rollup": "^2.77.2" + }, + "peerDependencies": { + "eslint": ">=7", + "vite": ">=2" + } + }, + "node_modules/vite-plugin-eslint/node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/examples/authz/3rd-party-app/package.json b/examples/authz/3rd-party-app/package.json new file mode 100644 index 0000000..12045de --- /dev/null +++ b/examples/authz/3rd-party-app/package.json @@ -0,0 +1,33 @@ +{ + "name": "pubky-auth-3rd-party", + "description": "Demo 3rd party app to show case Pubky Authorization", + "type": "module", + "scripts": { + "start": "npm run dev", + "dev": "vite --host --open", + "lint": "eslint --ext .js ./", + "lint:fix": "eslint --fix --ext .js ./", + "format": "prettier --write \"./**/*.{js,jsx,json,css}\"", + "build": "vite build", + "preview": "vite preview --open", + "buildpreview": "vite build && vite preview --open" + }, + "devDependencies": { + "autoprefixer": "^10.4.20", + "cssnano": "^7.0.5", + "eslint": "^8.57.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.2.1", + "postcss": "^8.4.41", + "postcss-nesting": "^13.0.0", + "prettier": "^3.3.3", + "vite": "5.4.2", + "vite-plugin-eslint": "^1.8.1" + }, + "dependencies": { + "qrcode": "^1.5.4", + "the-new-css-reset": "^1.11.3" + } +} diff --git a/examples/authz/3rd-party-app/postcss.config.cjs b/examples/authz/3rd-party-app/postcss.config.cjs new file mode 100644 index 0000000..cb76631 --- /dev/null +++ b/examples/authz/3rd-party-app/postcss.config.cjs @@ -0,0 +1,13 @@ +// For more information about PostCSS configuration files +// or the correct property values of each plugin, +// check each plugin's documentation. + +module.exports = { + plugins: { + 'postcss-nesting': {}, + autoprefixer: {}, + cssnano: { + preset: 'default' + }, + }, +}; diff --git a/examples/authz/3rd-party-app/public/screenshot.png b/examples/authz/3rd-party-app/public/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1932778ac6969bfcc40bb54b02f3b7e91fbcd360 GIT binary patch literal 70373 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 M)78&qol`;+0CoXfW&i*H literal 0 HcmV?d00001 diff --git a/examples/authz/3rd-party-app/public/vite.svg b/examples/authz/3rd-party-app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/authz/3rd-party-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/authz/3rd-party-app/src/js/main.js b/examples/authz/3rd-party-app/src/js/main.js new file mode 100644 index 0000000..77ebca1 --- /dev/null +++ b/examples/authz/3rd-party-app/src/js/main.js @@ -0,0 +1,69 @@ +import 'the-new-css-reset/css/reset.css'; +import '../styles/style.css'; +import QRCode from 'qrcode'; + +globalThis.pubkyAuthWidgetState = { + open: false, + qr: false +}; + +render(); + +function widget() { + return ` +

+ + +
+ `; +} + +globalThis.flip = () => { + globalThis.pubkyAuthWidgetState.open = + !globalThis.pubkyAuthWidgetState.open; + + + const widget = document.querySelector("#widget"); + widget.classList = widget.classList.contains("open") + ? [...widget.classList].filter(n => n !== "open") + : [...widget.classList, "open"]; + + if (!globalThis.pubkyAuthWidgetState.qr) { + let qrURL = "pubky:auth?cb=https://demo.httprelay.io/link/rxfa6k2k5"; + + const canvas = document.getElementById("qr"); + console.log({ qrURL, canvas }) + + QRCode.toCanvas(canvas, qrURL, { + margin: 2, + scale: 8, + + color: { + light: '#fff', + dark: '#000', + }, + }); + + globalThis.pubkyAuthWidgetState.qr = true + } +}; + +function render() { + document.querySelector('#app').innerHTML = ` +
+

Third Party app!

+

You are NOT logged in.

+
+ ${widget()} +`; +} diff --git a/examples/authz/3rd-party-app/src/styles/style.css b/examples/authz/3rd-party-app/src/styles/style.css new file mode 100644 index 0000000..0a8a075 --- /dev/null +++ b/examples/authz/3rd-party-app/src/styles/style.css @@ -0,0 +1,134 @@ +:root { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: white; + + background: radial-gradient( + circle, + transparent 20%, + #151718 20%, + #151718 80%, + transparent 80%, + transparent + ), + radial-gradient( + circle, + transparent 20%, + #151718 20%, + #151718 80%, + transparent 80%, + transparent + ) + 25px 25px, + linear-gradient(#202020 1px, transparent 2px) 0 -1px, + linear-gradient(90deg, #202020 1px, #151718 2px) -1px 0; + background-size: 50px 50px, 50px 50px, 25px 25px, 25px 25px; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 20rem; + min-height: 100vh; + font-family: var(--font-family); +} + +h1 { + font-weight: bold; + font-size: 3.2rem; + line-height: 1.1; +} + +button { + cursor: pointer +} + +#app { + max-width: 80rem; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +#widget { + position: fixed; + top: 1rem; + right: 1rem; + + background-color:red; + + z-index: 99999; + overflow: hidden; + background: rgba(43, 43, 43, .7372549019607844); + border: 1px solid #3c3c3c; + box-shadow: 0 10px 34px -10px rgba(236, 243, 222, .05); + border-radius: 8px; + padding: 1rem; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + + width: 8rem; + height: 3.31rem; + + will-change: height,width; + transition-property: height, width; + transition-duration: 200ms; + transition-timing-function: ease-in; +} + +#widget.open{ + width: 22rem; + height: 30.3rem; +} + + +#widget #widget-content{ + display: none +} + +#widget.open #widget-content{ + display: block +} + +#widget p { + font-size: 0.8rem; + line-height: 1.6rem; + text-align: center; + color: #fff; + opacity: .3; +} + +.card { + background: #3b3b3b; + border-radius: 5px; + padding: .625rem; + margin-top: 1rem; + display: flex; + justify-content: space-between; +} + +.url p { + display: flex; + align-items: center; + + line-height: 1!important; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; +} + +@media (prefers-color-scheme: light) { + :root { + color: var(--color-dark); + background-color: var(--color-light); + } + .author:hover { + color: var(--color-primary-hover); + } + button, + .gitRepo { + background-color: var(--color-button-light); + } +} diff --git a/examples/authz/3rd-party-app/vite.config.js b/examples/authz/3rd-party-app/vite.config.js new file mode 100644 index 0000000..d77ee50 --- /dev/null +++ b/examples/authz/3rd-party-app/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import eslint from 'vite-plugin-eslint'; + +export default defineConfig({ + publicDir: 'public', + root: './', + build: { + outDir: 'dist', + }, + plugins: [ + eslint({ cache: false }), + ], +}); From 041a86a2d36796e9525401068b79c00057fb8e3b Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 30 Aug 2024 09:22:29 +0300 Subject: [PATCH 094/125] test(pubky): use bootstrap as a resolver to help with flaky tests --- pubky/src/native.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 547f8db..f203bfd 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -2,7 +2,7 @@ use std::time::Duration; use ::pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PublicKey, Settings, SignedPacket, + PkarrClient, PublicKey, SignedPacket, }; use bytes::Bytes; use pkarr::Keypair; @@ -74,23 +74,24 @@ impl PubkyClient { } pub fn test(testnet: &Testnet) -> Self { - Self { - http: reqwest::Client::builder() - .cookie_store(true) - .user_agent(DEFAULT_USER_AGENT) - .build() - .unwrap(), - pkarr: PkarrClient::new(Settings { - dht: DhtSettings { - request_timeout: Some(Duration::from_millis(100)), - bootstrap: Some(testnet.bootstrap.to_owned()), - ..DhtSettings::default() - }, - ..Settings::default() + let pkarr = PkarrClient::builder() + .dht_settings(DhtSettings { + request_timeout: Some(Duration::from_millis(100)), + bootstrap: Some(testnet.bootstrap.to_owned()), + ..DhtSettings::default() }) + .resolvers(testnet.bootstrap.clone().into()) + .build() .unwrap() - .as_async(), - } + .as_async(); + + let http = reqwest::Client::builder() + .cookie_store(true) + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(); + + Self { http, pkarr } } // === Auth === From 61ff688dff0c8ed9540e8d43f2f899e9302e55df Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 30 Aug 2024 09:22:29 +0300 Subject: [PATCH 095/125] test(pubky): use bootstrap as a resolver to help with flaky tests --- pubky/src/native.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 547f8db..f203bfd 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -2,7 +2,7 @@ use std::time::Duration; use ::pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PublicKey, Settings, SignedPacket, + PkarrClient, PublicKey, SignedPacket, }; use bytes::Bytes; use pkarr::Keypair; @@ -74,23 +74,24 @@ impl PubkyClient { } pub fn test(testnet: &Testnet) -> Self { - Self { - http: reqwest::Client::builder() - .cookie_store(true) - .user_agent(DEFAULT_USER_AGENT) - .build() - .unwrap(), - pkarr: PkarrClient::new(Settings { - dht: DhtSettings { - request_timeout: Some(Duration::from_millis(100)), - bootstrap: Some(testnet.bootstrap.to_owned()), - ..DhtSettings::default() - }, - ..Settings::default() + let pkarr = PkarrClient::builder() + .dht_settings(DhtSettings { + request_timeout: Some(Duration::from_millis(100)), + bootstrap: Some(testnet.bootstrap.to_owned()), + ..DhtSettings::default() }) + .resolvers(testnet.bootstrap.clone().into()) + .build() .unwrap() - .as_async(), - } + .as_async(); + + let http = reqwest::Client::builder() + .cookie_store(true) + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(); + + Self { http, pkarr } } // === Auth === From 7fc0e00e41529305547cebc37f36b336048f9c4b Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 30 Aug 2024 10:46:52 +0300 Subject: [PATCH 096/125] examples(authz): switch from plain js to web components --- Cargo.lock | 4 - examples/authz/3rd-party-app/.eslintignore | 5 - examples/authz/3rd-party-app/.eslintrc.json | 26 - .../3rd-party-app/.github/dependabot.yml | 6 - examples/authz/3rd-party-app/.gitignore | 667 +-- examples/authz/3rd-party-app/.prettierignore | 2 - examples/authz/3rd-party-app/.prettierrc | 8 - examples/authz/3rd-party-app/README.md | 132 - examples/authz/3rd-party-app/index.html | 13 +- .../authz/3rd-party-app/package-lock.json | 4283 +---------------- examples/authz/3rd-party-app/package.json | 30 +- .../authz/3rd-party-app/postcss.config.cjs | 13 - .../authz/3rd-party-app/public/screenshot.png | Bin 70373 -> 0 bytes examples/authz/3rd-party-app/public/vite.svg | 2 +- examples/authz/3rd-party-app/src/index.css | 48 + .../3rd-party-app/src/pubky-auth-widget.js | 166 + examples/authz/3rd-party-app/vite.config.js | 13 - 17 files changed, 298 insertions(+), 5120 deletions(-) delete mode 100644 examples/authz/3rd-party-app/.eslintignore delete mode 100644 examples/authz/3rd-party-app/.eslintrc.json delete mode 100644 examples/authz/3rd-party-app/.github/dependabot.yml delete mode 100644 examples/authz/3rd-party-app/.prettierignore delete mode 100644 examples/authz/3rd-party-app/.prettierrc delete mode 100644 examples/authz/3rd-party-app/README.md delete mode 100644 examples/authz/3rd-party-app/postcss.config.cjs delete mode 100644 examples/authz/3rd-party-app/public/screenshot.png create mode 100644 examples/authz/3rd-party-app/src/index.css create mode 100644 examples/authz/3rd-party-app/src/pubky-auth-widget.js delete mode 100644 examples/authz/3rd-party-app/vite.config.js diff --git a/Cargo.lock b/Cargo.lock index b3862bf..df600ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,10 +135,6 @@ dependencies = [ "critical-section", ] -[[package]] -name = "authenticator" -version = "0.1.0" - [[package]] name = "autocfg" version = "1.3.0" diff --git a/examples/authz/3rd-party-app/.eslintignore b/examples/authz/3rd-party-app/.eslintignore deleted file mode 100644 index 093420c..0000000 --- a/examples/authz/3rd-party-app/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -.vscode -dist -node_modules -vite.config.ts -postcss.config.js \ No newline at end of file diff --git a/examples/authz/3rd-party-app/.eslintrc.json b/examples/authz/3rd-party-app/.eslintrc.json deleted file mode 100644 index 2dc6d48..0000000 --- a/examples/authz/3rd-party-app/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "root": true, - "env": { - "browser": true, - "es2023": true, - "node": true - }, - "extends": [ - "plugin:import/recommended", - "eslint-config-prettier" - ], - "plugins": ["import"], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [".js"], - "path": ["src"], - "moduleDirectory": ["node_modules"] - } - } - } -} diff --git a/examples/authz/3rd-party-app/.github/dependabot.yml b/examples/authz/3rd-party-app/.github/dependabot.yml deleted file mode 100644 index 7b2ee3a..0000000 --- a/examples/authz/3rd-party-app/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: npm - directory: "/" - schedule: - interval: daily diff --git a/examples/authz/3rd-party-app/.gitignore b/examples/authz/3rd-party-app/.gitignore index e6552dd..a547bf3 100644 --- a/examples/authz/3rd-party-app/.gitignore +++ b/examples/authz/3rd-party-app/.gitignore @@ -1,669 +1,24 @@ -# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,intellij,node -# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,intellij,node - -### Intellij ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Intellij Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -# Azure Toolkit for IntelliJ plugin -# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij -.idea/**/azureSettings.xml - -### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* lerna-debug.log* -.pnpm-debug.log* -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt +node_modules dist +dist-ssr +*.local -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -### Node Patch ### -# Serverless Webpack directories -.webpack/ - -# Optional stylelint cache - -# SvelteKit build / generate output -.svelte-kit - -### VisualStudioCode ### +# Editor directories and files .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json !.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -### VisualStudio ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser +.idea +.DS_Store *.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -*.code-workspace - -# Local History for Visual Studio Code - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml - -### VisualStudio Patch ### -# Additional files built by Visual Studio - -# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,intellij,node +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/authz/3rd-party-app/.prettierignore b/examples/authz/3rd-party-app/.prettierignore deleted file mode 100644 index db4c6d9..0000000 --- a/examples/authz/3rd-party-app/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules \ No newline at end of file diff --git a/examples/authz/3rd-party-app/.prettierrc b/examples/authz/3rd-party-app/.prettierrc deleted file mode 100644 index 986cbad..0000000 --- a/examples/authz/3rd-party-app/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 80, - "tabWidth": 4, - "singleQuote": true, - "trailingComma": "all", - "useTabs": false, - "semi": true -} diff --git a/examples/authz/3rd-party-app/README.md b/examples/authz/3rd-party-app/README.md deleted file mode 100644 index b0c5b00..0000000 --- a/examples/authz/3rd-party-app/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Vite Vanilla JS Template - -![screenshot](/public/screenshot.png) - -Initially built for personal use, I created this template for starting a new project with Vite.js and Vanilla Javascript. It is already set up with standard development tools like ESLint and Prettier for easy code formatting and linting, with Vite for a robust, modern build process. - -## Dependencies - -This template uses the following dependencies: - -- **[Vite](https://vitejs.dev/):** A next-generation frontend build tool that offers a fast dev server and optimized builds. -- **[ESLint](https://eslint.org/):** An open-source JavaScript linting utility that helps maintain a consistent code style. -- **[Prettier](https://prettier.io/):** An opinionated code formatter that enforces a consistent style across your project. -- **[eslint-config-airbnb-base](https://www.npmjs.com/package/eslint-config-airbnb-base) and [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier):** ESLint configurations adhering to Airbnb's base JS style guide and disabling stylistic rules that might conflict with Prettier. -- **[eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import) and [eslint-plugin-prettier](https://www.npmjs.com/package/eslint-plugin-prettier):** ESLint plugins that enforce ES2015+ import/export syntax and integrate Prettier with ESLint. -- **[autoprefixer](https://www.npmjs.com/package/autoprefixer) and [postcss](https://postcss.org/):** Autoprefixer automatically adds vendor prefixes to CSS, while PostCSS provides a way to transform CSS with JavaScript. -- **[cssnano](https://cssnano.github.io/cssnano/):** A tool that helps to compress and optimize CSS files. -- **[postcss-nesting](https://www.npmjs.com/package/postcss-nesting):** A PostCSS plugin that allows you to use modern CSS Nesting in your stylesheets. -- **[vite-plugin-eslint](https://www.npmjs.com/package/vite-plugin-eslint):** Integrates ESLint into the Vite build process for on-the-fly linting. -- **[the-new-css-reset](https://elad2412.github.io/the-new-css-reset/):** A modern, CSS reset for your styles. - -## Cloning - -1. To start using this template, clone the repository with this command: - -```bash -git clone https://github.com/Barata-Ribeiro/vite-vanilla-js-template.git -``` - -2. Then proceed to the folder and install dependencies: - -```bash -cd vite-vanilla-js-template -npm install -``` - -**or** - -```bash -npm install -g degit # if you don't have degit installed... - -#degit documentation at: https://github.com/Rich-Harris/degit -``` - -1. Use 'degit' to create a folder project using this template: - -```bash -degit Barata-Ribeiro/vite-vanilla-js-template your-project-name -``` - -2. Then proceed to the folder and install dependencies: - -```bash -cd your-project-name -npm install -``` - - -## Post-Cloning Steps - -After cloning the template, make sure to clean up and update the following: - -1. Remove the .git directory and run `git init` to clean the commit history. -2. Clean up the README.md file. -3. Adapt the LICENSE file to your project. -4. Delete `public/vite.svg`, `public/screenshot`, `src/assets/images/javascript.svg`, and `src/assets/images/vite.svg`. -5. Delete the content from `src/styles/style.css`. -6. In the `src/js/main.js` file, leave only these import statements: `import "../../styles/style.css";` and `import "the-new-css-reset/css/reset.css";`. -7. Adapt the `package.json` file with your project's own information. -8. Delete the .github folder. - -## Scripts - -Use the following scripts for your development workflow: - -```bash -# Start the development server -npm run dev - -# Checks your code for any linting errors -npm run lint - -# Tries to automatically fix any linting errors present in your code -npm run lint:fix - -# Formats your code in a consistent, predefined style using Prettier -npm run format - -# Build for production -npm run build - -# Preview the build -npm run preview - -# Build and preview the project -npm run buildpreview -``` - -## Folder Structure - -This is the structure of the project: - -```plaintext -/ -├── .github # Github actions and workflows -├── node_modules # Node.js dependencies for the project. -├── public # Public assets and resources -├── src # Source code -│ ├── assets # General assets for your project -│ │ ├── images # Store your images here -│ ├── js # Javascript files of your project -│ ├── styles # CSS styles for your project -├── .editorconfig # Configuration for the EditorConfig plugin -├── .eslintignore # Files to be ignored by ESLint -├── .eslintrc.json # Configuration for ESLint -├── .gitignore # Files and folders to be ignored by Git -├── .prettierignore # Files to be ignored by Prettier -├── .prettierrc # Configuration for Prettier -├── index.html # The HTML file for your project -├── LICENSE # The license for your project -├── package-lock.json # Lockfile for your project's dependencies -├── package.json # Defines your project and its dependencies -├── postcss.config.cjs # Configuration for PostCSS -├── README.md # This file -├── vite.config.js # Configuration for Vite -``` - -## License - -This template was created under the [MIT License](LICENSE.md). - -**Happy coding!** 👨‍💻 diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html index 79559cf..0a4a5da 100644 --- a/examples/authz/3rd-party-app/index.html +++ b/examples/authz/3rd-party-app/index.html @@ -4,12 +4,15 @@ - Vite.js Vanilla Template + Vite + Lit + + - -
-
- + +
+

Third Party app!

+

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

+
diff --git a/examples/authz/3rd-party-app/package-lock.json b/examples/authz/3rd-party-app/package-lock.json index 6058b0c..5ce8a32 100644 --- a/examples/authz/3rd-party-app/package-lock.json +++ b/examples/authz/3rd-party-app/package-lock.json @@ -1,136 +1,18 @@ { "name": "pubky-auth-3rd-party", - "version": "1.0.0", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pubky-auth-3rd-party", + "version": "0.0.0", "dependencies": { - "qrcode": "^1.5.4", - "the-new-css-reset": "^1.11.3" + "lit": "^3.2.0", + "qrcode": "^1.5.4" }, "devDependencies": { - "autoprefixer": "^10.4.20", - "cssnano": "^7.0.5", - "eslint": "^8.57.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-prettier": "^5.2.1", - "postcss": "^8.4.41", - "postcss-nesting": "^13.0.0", - "prettier": "^3.3.3", - "vite": "5.4.2", - "vite-plugin-eslint": "^1.8.1" - } - }, - "node_modules/@csstools/selector-resolve-nested": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-2.0.0.tgz", - "integrity": "sha512-oklSrRvOxNeeOW1yARd4WNCs/D09cQjunGZUgSq6vM8GpzFswN+8rBZyJA29YFZhOTQ6GFzxgLDNtVbt9wPZMA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.1.0" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz", - "integrity": "sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.1.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "vite": "^5.4.2" } }, "node_modules/@esbuild/darwin-arm64": { @@ -149,469 +31,19 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", + "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==" }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, + "node_modules/@lit/reactive-element": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@lit-labs/ssr-dom-shim": "^1.2.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dev": true, - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz", - "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz", - "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", @@ -625,254 +57,16 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz", - "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz", - "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz", - "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz", - "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz", - "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz", - "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz", - "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz", - "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", - "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz", - "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz", - "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz", - "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz", - "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "node_modules/ansi-regex": { "version": "5.0.1", @@ -896,260 +90,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1158,54 +98,6 @@ "node": ">=6" } }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001649", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz", - "integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -1232,289 +124,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "dev": true - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.5.tgz", - "integrity": "sha512-Aq0vqBLtpTT5Yxj+hLlLfNPFuRQCDIjx5JQAhhaedQKLNDvDGeVziF24PS+S1f0Z5KCxWvw0QVI3VNHNBITxVQ==", - "dev": true, - "dependencies": { - "cssnano-preset-default": "^7.0.5", - "lilconfig": "^3.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-preset-default": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.5.tgz", - "integrity": "sha512-Jbzja0xaKwc5JzxPQoc+fotKpYtWEu4wQLMQe29CM0FjjdRjA4omvbGHl2DTGgARKxSTpPssBsok+ixv8uTBqw==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.0", - "postcss-calc": "^10.0.1", - "postcss-colormin": "^7.0.2", - "postcss-convert-values": "^7.0.3", - "postcss-discard-comments": "^7.0.2", - "postcss-discard-duplicates": "^7.0.1", - "postcss-discard-empty": "^7.0.0", - "postcss-discard-overridden": "^7.0.0", - "postcss-merge-longhand": "^7.0.3", - "postcss-merge-rules": "^7.0.3", - "postcss-minify-font-values": "^7.0.0", - "postcss-minify-gradients": "^7.0.0", - "postcss-minify-params": "^7.0.2", - "postcss-minify-selectors": "^7.0.3", - "postcss-normalize-charset": "^7.0.0", - "postcss-normalize-display-values": "^7.0.0", - "postcss-normalize-positions": "^7.0.0", - "postcss-normalize-repeat-style": "^7.0.0", - "postcss-normalize-string": "^7.0.0", - "postcss-normalize-timing-functions": "^7.0.0", - "postcss-normalize-unicode": "^7.0.2", - "postcss-normalize-url": "^7.0.0", - "postcss-normalize-whitespace": "^7.0.0", - "postcss-ordered-values": "^7.0.1", - "postcss-reduce-initial": "^7.0.2", - "postcss-reduce-transforms": "^7.0.0", - "postcss-svgo": "^7.0.1", - "postcss-unique-selectors": "^7.0.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", - "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", - "dev": true, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -1523,274 +132,16 @@ "node": ">=0.10.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", - "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", - "dev": true - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1829,443 +180,18 @@ "@esbuild/win32-x64": "0.21.5" } }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dependencies": { - "locate-path": "^6.0.0", + "locate-path": "^5.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2280,42 +206,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2324,375 +214,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2701,296 +222,45 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "node_modules/lit": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz", + "integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==", "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.1.0", + "lit-html": "^3.2.0" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-yaml": { + "node_modules/lit-element": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.0.tgz", + "integrity": "sha512-gSejRUQJuMQjV2Z59KAS/D4iElUhwKpIyJvZ9w+DIagIQjfJnhR20h2Q5ddpzXGS+fF0tMZ/xEYGMnKmaI/iww==", "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.2.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, + "node_modules/lit-html": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.0.tgz", + "integrity": "sha512-pwT/HwoxqI9FggTrYVarkBKFN9MlTUpLrDHubTmW4SrkL3kkqW5gxwbxMMUnbbRHBC0WTZnYHcjDSCM559VyfA==", "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" + "@types/trusted-types": "^2.0.2" } }, "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -3009,192 +279,29 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dependencies": { - "yocto-queue": "^0.1.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dependencies": { - "p-limit": "^3.0.2" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/p-try": { @@ -3205,18 +312,6 @@ "node": ">=6" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3225,48 +320,12 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -3275,15 +334,6 @@ "node": ">=10.13.0" } }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", @@ -3312,508 +362,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-calc": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.1.tgz", - "integrity": "sha512-pp1Z3FxtxA+xHAoWXcOXgnBN1WPu4ZiJ5LWGjKyf9MMreagAsaTUtnqFK1y1sHhyJddAkYTPu6XSuLgb3oYCjw==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.1.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12 || ^20.9 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.38" - } - }, - "node_modules/postcss-colormin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", - "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-convert-values": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.3.tgz", - "integrity": "sha512-yJhocjCs2SQer0uZ9lXTMOwDowbxvhwFVrZeS6NPEij/XXthl73ggUmfwVvJM+Vaj5gtCKJV1jiUu4IhAUkX/Q==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-comments": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.2.tgz", - "integrity": "sha512-/Hje9Ls1IYcB9duELO/AyDUJI6aQVY3h5Rj1ziXgaLYCTi1iVBLnjg/TS0D6NszR/kDG6I86OwLmAYe+bvJjiQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", - "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", - "dev": true, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-empty": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", - "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", - "dev": true, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", - "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", - "dev": true, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.3.tgz", - "integrity": "sha512-8waYomFxshdv6M9Em3QRM9MettRLDRcH2JQi2l0Z1KlYD/vhal3gbkeSES0NuACXOlZBB0V/B0AseHZaklzWOA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.3" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-rules": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.3.tgz", - "integrity": "sha512-2eSas2p3voPxNfdI5sQrvIkMaeUHpVc3EezgVs18hz/wRTQAC9U99tp9j3W5Jx9/L3qHkEDvizEx/LdnmumIvQ==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.0", - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", - "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", - "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", - "dev": true, - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-params": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", - "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.3.tgz", - "integrity": "sha512-SxTgUQSgBk6wEqzQZKEv1xQYIp9UBju6no9q+npohzSdhuSICQdkqmD1UMKkZWItS3olJSJMDDEY9WOJ5oGJew==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-nesting": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.0.tgz", - "integrity": "sha512-TCGQOizyqvEkdeTPM+t6NYwJ3EJszYE/8t8ILxw/YoeUvz2rz7aM8XTAmBWh9/DJjfaaabL88fWrsVHSPF2zgA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "dependencies": { - "@csstools/selector-resolve-nested": "^2.0.0", - "@csstools/selector-specificity": "^4.0.0", - "postcss-selector-parser": "^6.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", - "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", - "dev": true, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", - "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", - "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", - "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-string": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", - "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", - "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", - "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", - "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", - "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-ordered-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", - "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", - "dev": true, - "dependencies": { - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", - "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", - "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", - "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.3.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.2.tgz", - "integrity": "sha512-CjSam+7Vf8cflJQsHrMS0P2hmy9u0+n/P001kb5eAszLmhjMqrt/i5AqQuNFihhViwDvEAezqTmXqaYXL2ugMw==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -3830,44 +378,6 @@ "node": ">=10.13.0" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3881,57 +391,6 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", @@ -3967,149 +426,11 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -4132,55 +453,6 @@ "node": ">=8" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4192,294 +464,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylehacks": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.3.tgz", - "integrity": "sha512-4DqtecvI/Nd+2BCvW9YEF6lhBN5UM50IJ1R3rnEAhBwbCKf4VehRf+uqvnVArnBayjYD/WtT3g0G/HSRxWfTRg==", - "dev": true, - "dependencies": { - "browserslist": "^4.23.3", - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "dev": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", - "dev": true, - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/the-new-css-reset": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/the-new-css-reset/-/the-new-css-reset-1.11.3.tgz", - "integrity": "sha512-61SB81vu9foUyEIqoU1CeqxrdlsVjJojj/CBXoG8BdvlKFsllB0Rza63DblnRqH+3uttPj3FGWo7+c9nu7MT+A==" - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, "node_modules/vite": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", @@ -4539,100 +523,11 @@ } } }, - "node_modules/vite-plugin-eslint": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz", - "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^4.2.1", - "@types/eslint": "^8.4.5", - "rollup": "^2.77.2" - }, - "peerDependencies": { - "eslint": ">=7", - "vite": ">=2" - } - }, - "node_modules/vite-plugin-eslint/node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -4646,12 +541,6 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -4689,66 +578,6 @@ "engines": { "node": ">=6" } - }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/examples/authz/3rd-party-app/package.json b/examples/authz/3rd-party-app/package.json index 12045de..11bb8b8 100644 --- a/examples/authz/3rd-party-app/package.json +++ b/examples/authz/3rd-party-app/package.json @@ -1,33 +1,19 @@ { "name": "pubky-auth-3rd-party", - "description": "Demo 3rd party app to show case Pubky Authorization", + "private": true, + "version": "0.0.0", "type": "module", "scripts": { "start": "npm run dev", "dev": "vite --host --open", - "lint": "eslint --ext .js ./", - "lint:fix": "eslint --fix --ext .js ./", - "format": "prettier --write \"./**/*.{js,jsx,json,css}\"", "build": "vite build", - "preview": "vite preview --open", - "buildpreview": "vite build && vite preview --open" - }, - "devDependencies": { - "autoprefixer": "^10.4.20", - "cssnano": "^7.0.5", - "eslint": "^8.57.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-prettier": "^5.2.1", - "postcss": "^8.4.41", - "postcss-nesting": "^13.0.0", - "prettier": "^3.3.3", - "vite": "5.4.2", - "vite-plugin-eslint": "^1.8.1" + "preview": "vite preview" }, "dependencies": { - "qrcode": "^1.5.4", - "the-new-css-reset": "^1.11.3" + "lit": "^3.2.0", + "qrcode": "^1.5.4" + }, + "devDependencies": { + "vite": "^5.4.2" } } diff --git a/examples/authz/3rd-party-app/postcss.config.cjs b/examples/authz/3rd-party-app/postcss.config.cjs deleted file mode 100644 index cb76631..0000000 --- a/examples/authz/3rd-party-app/postcss.config.cjs +++ /dev/null @@ -1,13 +0,0 @@ -// For more information about PostCSS configuration files -// or the correct property values of each plugin, -// check each plugin's documentation. - -module.exports = { - plugins: { - 'postcss-nesting': {}, - autoprefixer: {}, - cssnano: { - preset: 'default' - }, - }, -}; diff --git a/examples/authz/3rd-party-app/public/screenshot.png b/examples/authz/3rd-party-app/public/screenshot.png deleted file mode 100644 index 1932778ac6969bfcc40bb54b02f3b7e91fbcd360..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70373 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 M)78&qol`;+0CoXfW&i*H diff --git a/examples/authz/3rd-party-app/public/vite.svg b/examples/authz/3rd-party-app/public/vite.svg index e7b8dfb..ee9fada 100644 --- a/examples/authz/3rd-party-app/public/vite.svg +++ b/examples/authz/3rd-party-app/public/vite.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/examples/authz/3rd-party-app/src/index.css b/examples/authz/3rd-party-app/src/index.css new file mode 100644 index 0000000..809fde4 --- /dev/null +++ b/examples/authz/3rd-party-app/src/index.css @@ -0,0 +1,48 @@ +:root { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: white; + + background: radial-gradient( + circle, + transparent 20%, + #151718 20%, + #151718 80%, + transparent 80%, + transparent + ), + radial-gradient( + circle, + transparent 20%, + #151718 20%, + #151718 80%, + transparent 80%, + transparent + ) + 25px 25px, + linear-gradient(#202020 1px, transparent 2px) 0 -1px, + linear-gradient(90deg, #202020 1px, #151718 2px) -1px 0; + background-size: 50px 50px, 50px 50px, 25px 25px, 25px 25px; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 20rem; + min-height: 100vh; + font-family: var(--font-family); +} + +h1 { + font-weight: bold; + font-size: 3.2rem; + line-height: 1.1; +} + +main { + max-width: 80rem; + margin: 0 auto; + padding: 2rem; + text-align: center; +} diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js new file mode 100644 index 0000000..1c94053 --- /dev/null +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -0,0 +1,166 @@ +import { LitElement, css, html } from 'lit' +import { createRef, ref } from 'lit/directives/ref.js'; +import QRCode from 'qrcode' + +/** + * An example element. + * + * @csspart button - The button + */ +export class PubkyAuthWidget extends LitElement { + static get properties() { + return { + open: { type: Boolean }, + } + } + + canvasRef = createRef(); + + constructor() { + super() + this.open = false; + this.authUrl = "pubky:auth?cb=https://demo.httprelay.io/link/rxfa6k2k5"; + + } + + render() { + return html` +

+ +
+
+

Scan or copy Pubky auth URL

+
+ +
+ +

${this.authUrl}

+ +
+
+
+ ` + } + + _setQr(canvas) { + console.log(canvas, this.authUrl); + + QRCode.toCanvas(canvas, this.authUrl, { + margin: 2, + scale: 8, + + color: { + light: '#fff', + dark: '#000', + }, + }); + } + + _switchOpen() { + this.open = !this.open + } + + static get styles() { + return css` + button { + background: none; + border: none; + color: inherit; + cursor: pointer; + } + + p { + margin: 0; + } + + /** End reset */ + + #widget { + font-size: 10px; + color: white; + + position: fixed; + top: 1rem; + right: 1rem; + + background-color:red; + + z-index: 99999; + overflow: hidden; + background: rgba(43, 43, 43, .7372549019607844); + border: 1px solid #3c3c3c; + box-shadow: 0 10px 34px -10px rgba(236, 243, 222, .05); + border-radius: 8px; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + + width: 10em; + height: 4em; + + will-change: height,width; + transition-property: height, width; + transition-duration: 200ms; + transition-timing-function: ease-in; + } + + #widget.open{ + width: 35em; + height: 47em; + } + + .header { + width: 100%; + padding: 1em; + } + + #widget-content{ + padding: 1.6em + } + + #widget p { + font-size: 1.4em; + line-height: 1em; + text-align: center; + color: #fff; + opacity: .3; + } + + #qr { + width: 30em !important; + height: 30em !important; + } + + .card { + background: #3b3b3b; + border-radius: 5px; + padding: 1em; + margin-top: 1em; + display: flex; + justify-content: center; + align-items: center; + } + + .url p { + display: flex; + align-items: center; + + line-height: 1!important; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; + } + + .line { + height: 1px; + background-color: #3b3b3b; + flex: 1 1; + } + ` + } +} + +window.customElements.define('pubky-auth-widget', PubkyAuthWidget) diff --git a/examples/authz/3rd-party-app/vite.config.js b/examples/authz/3rd-party-app/vite.config.js deleted file mode 100644 index d77ee50..0000000 --- a/examples/authz/3rd-party-app/vite.config.js +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vite'; -import eslint from 'vite-plugin-eslint'; - -export default defineConfig({ - publicDir: 'public', - root: './', - build: { - outDir: 'dist', - }, - plugins: [ - eslint({ cache: false }), - ], -}); From eeb2784da77939ce30f7695f06f385fe506a0d95 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 30 Aug 2024 11:11:02 +0300 Subject: [PATCH 097/125] examples(authz): style fixes --- examples/authz/3rd-party-app/src/js/main.js | 69 --------- .../3rd-party-app/src/pubky-auth-widget.js | 48 +++++-- .../authz/3rd-party-app/src/styles/style.css | 134 ------------------ 3 files changed, 35 insertions(+), 216 deletions(-) delete mode 100644 examples/authz/3rd-party-app/src/js/main.js delete mode 100644 examples/authz/3rd-party-app/src/styles/style.css diff --git a/examples/authz/3rd-party-app/src/js/main.js b/examples/authz/3rd-party-app/src/js/main.js deleted file mode 100644 index 77ebca1..0000000 --- a/examples/authz/3rd-party-app/src/js/main.js +++ /dev/null @@ -1,69 +0,0 @@ -import 'the-new-css-reset/css/reset.css'; -import '../styles/style.css'; -import QRCode from 'qrcode'; - -globalThis.pubkyAuthWidgetState = { - open: false, - qr: false -}; - -render(); - -function widget() { - return ` -
- - -
- `; -} - -globalThis.flip = () => { - globalThis.pubkyAuthWidgetState.open = - !globalThis.pubkyAuthWidgetState.open; - - - const widget = document.querySelector("#widget"); - widget.classList = widget.classList.contains("open") - ? [...widget.classList].filter(n => n !== "open") - : [...widget.classList, "open"]; - - if (!globalThis.pubkyAuthWidgetState.qr) { - let qrURL = "pubky:auth?cb=https://demo.httprelay.io/link/rxfa6k2k5"; - - const canvas = document.getElementById("qr"); - console.log({ qrURL, canvas }) - - QRCode.toCanvas(canvas, qrURL, { - margin: 2, - scale: 8, - - color: { - light: '#fff', - dark: '#000', - }, - }); - - globalThis.pubkyAuthWidgetState.qr = true - } -}; - -function render() { - document.querySelector('#app').innerHTML = ` -
-

Third Party app!

-

You are NOT logged in.

-
- ${widget()} -`; -} diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js index 1c94053..29f291f 100644 --- a/examples/authz/3rd-party-app/src/pubky-auth-widget.js +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -65,6 +65,16 @@ export class PubkyAuthWidget extends LitElement { static get styles() { return css` + * { + box-sizing: border-box; + } + + :host { + --full-width: 22rem; + --full-height: 31rem; + --header-height: 3rem; + } + button { background: none; border: none; @@ -79,7 +89,6 @@ export class PubkyAuthWidget extends LitElement { /** End reset */ #widget { - font-size: 10px; color: white; position: fixed; @@ -97,8 +106,8 @@ export class PubkyAuthWidget extends LitElement { -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); - width: 10em; - height: 4em; + width: 7rem; + height: var(--header-height); will-change: height,width; transition-property: height, width; @@ -107,42 +116,54 @@ export class PubkyAuthWidget extends LitElement { } #widget.open{ - width: 35em; - height: 47em; + width: var(--full-width); + height: var(--full-height); } .header { width: 100%; - padding: 1em; + height: var(--header-height); + display: flex; + justify-content: center; + align-items: center; } #widget-content{ - padding: 1.6em + width: var(--full-width); + padding: 0 1rem } #widget p { - font-size: 1.4em; - line-height: 1em; + font-size: .87rem; + line-height: 1rem; text-align: center; color: #fff; opacity: .3; + + /* Fix flash wrap in open animation */ + text-wrap: nowrap; } #qr { - width: 30em !important; - height: 30em !important; + width: 18em !important; + height: 18em !important; } .card { background: #3b3b3b; border-radius: 5px; - padding: 1em; - margin-top: 1em; + padding: 1rem; + margin-top: 1rem; display: flex; justify-content: center; align-items: center; } + .card.url { + padding: .625rem; + justify-content: space-between; + } + .url p { display: flex; align-items: center; @@ -158,6 +179,7 @@ export class PubkyAuthWidget extends LitElement { height: 1px; background-color: #3b3b3b; flex: 1 1; + margin-bottom: 1rem; } ` } diff --git a/examples/authz/3rd-party-app/src/styles/style.css b/examples/authz/3rd-party-app/src/styles/style.css deleted file mode 100644 index 0a8a075..0000000 --- a/examples/authz/3rd-party-app/src/styles/style.css +++ /dev/null @@ -1,134 +0,0 @@ -:root { - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', - Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - color: white; - - background: radial-gradient( - circle, - transparent 20%, - #151718 20%, - #151718 80%, - transparent 80%, - transparent - ), - radial-gradient( - circle, - transparent 20%, - #151718 20%, - #151718 80%, - transparent 80%, - transparent - ) - 25px 25px, - linear-gradient(#202020 1px, transparent 2px) 0 -1px, - linear-gradient(90deg, #202020 1px, #151718 2px) -1px 0; - background-size: 50px 50px, 50px 50px, 25px 25px, 25px 25px; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 20rem; - min-height: 100vh; - font-family: var(--font-family); -} - -h1 { - font-weight: bold; - font-size: 3.2rem; - line-height: 1.1; -} - -button { - cursor: pointer -} - -#app { - max-width: 80rem; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -#widget { - position: fixed; - top: 1rem; - right: 1rem; - - background-color:red; - - z-index: 99999; - overflow: hidden; - background: rgba(43, 43, 43, .7372549019607844); - border: 1px solid #3c3c3c; - box-shadow: 0 10px 34px -10px rgba(236, 243, 222, .05); - border-radius: 8px; - padding: 1rem; - -webkit-backdrop-filter: blur(8px); - backdrop-filter: blur(8px); - - width: 8rem; - height: 3.31rem; - - will-change: height,width; - transition-property: height, width; - transition-duration: 200ms; - transition-timing-function: ease-in; -} - -#widget.open{ - width: 22rem; - height: 30.3rem; -} - - -#widget #widget-content{ - display: none -} - -#widget.open #widget-content{ - display: block -} - -#widget p { - font-size: 0.8rem; - line-height: 1.6rem; - text-align: center; - color: #fff; - opacity: .3; -} - -.card { - background: #3b3b3b; - border-radius: 5px; - padding: .625rem; - margin-top: 1rem; - display: flex; - justify-content: space-between; -} - -.url p { - display: flex; - align-items: center; - - line-height: 1!important; - width: 90%; - overflow: hidden; - text-overflow: ellipsis; - text-wrap: nowrap; -} - -@media (prefers-color-scheme: light) { - :root { - color: var(--color-dark); - background-color: var(--color-light); - } - .author:hover { - color: var(--color-primary-hover); - } - button, - .gitRepo { - background-color: var(--color-button-light); - } -} From 467738bb235b8d59d377913e47c8f8f1c9a1a0d0 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 30 Aug 2024 11:25:05 +0300 Subject: [PATCH 098/125] test(pubky): increase request_timeout to avoid failing resolution --- pubky/src/native.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index f203bfd..91adb02 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -76,7 +76,7 @@ impl PubkyClient { pub fn test(testnet: &Testnet) -> Self { let pkarr = PkarrClient::builder() .dht_settings(DhtSettings { - request_timeout: Some(Duration::from_millis(100)), + request_timeout: Some(Duration::from_millis(500)), bootstrap: Some(testnet.bootstrap.to_owned()), ..DhtSettings::default() }) From 16771d662b148450cad97c2dd4fbebbc6b2c3d0c Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 30 Aug 2024 11:25:05 +0300 Subject: [PATCH 099/125] test(pubky): increase request_timeout to avoid failing resolution --- pubky/src/native.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index f203bfd..91adb02 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -76,7 +76,7 @@ impl PubkyClient { pub fn test(testnet: &Testnet) -> Self { let pkarr = PkarrClient::builder() .dht_settings(DhtSettings { - request_timeout: Some(Duration::from_millis(100)), + request_timeout: Some(Duration::from_millis(500)), bootstrap: Some(testnet.bootstrap.to_owned()), ..DhtSettings::default() }) From 8e70b2b0e63ff589b8eed487b78c26fe12f64548 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 30 Aug 2024 13:38:40 +0300 Subject: [PATCH 100/125] examples(authz): initial use of httprelay callback function --- examples/authz/3rd-party-app/index.html | 3 +- .../3rd-party-app/src/pubky-auth-widget.js | 70 ++++++++++++++++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html index 0a4a5da..abfe35a 100644 --- a/examples/authz/3rd-party-app/index.html +++ b/examples/authz/3rd-party-app/index.html @@ -9,7 +9,8 @@ - + +

Third Party app!

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

diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js index 29f291f..a01a1bb 100644 --- a/examples/authz/3rd-party-app/src/pubky-auth-widget.js +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -2,14 +2,26 @@ import { LitElement, css, html } from 'lit' import { createRef, ref } from 'lit/directives/ref.js'; import QRCode from 'qrcode' +const DEFAULT_HTTP_RELAY = "https://demo.httprelay.io/link" + /** - * An example element. - * - * @csspart button - The button */ export class PubkyAuthWidget extends LitElement { static get properties() { return { + /** + * Relay endpoint for the widget to receive Pubky AuthTokens + * + * Internally, a random channel ID will be generated and a + * GET request made for `${realy url}/${channelID}` + * + * If no relay is passed, the widget will use a default relay: + * https://demo.httprelay.io/link + */ + relay: { type: String }, + /** + * Widget's state (open or closed) + */ open: { type: Boolean }, } } @@ -17,10 +29,32 @@ export class PubkyAuthWidget extends LitElement { canvasRef = createRef(); constructor() { + // TODO: show error if the PubkyClient is not available! super() this.open = false; - this.authUrl = "pubky:auth?cb=https://demo.httprelay.io/link/rxfa6k2k5"; + } + connectedCallback() { + super.connectedCallback() + + // Verify it is a valid URL + const callbackUrl = this.relay ? + new URL( + // Remove trailing '/' + this.relay.endsWith("/") + ? this.relay.slice(0, this.relay.length - 1) + : this.relay + ) + : DEFAULT_HTTP_RELAY + + const channel = Math.random().toString(32).slice(2); + callbackUrl.pathname = callbackUrl.pathname + "/" + channel + + this.authUrl = `pubky:auth?cb=${callbackUrl.toString()}`; + + fetch(callbackUrl) + .catch(console.log) + .then(this._onCallback) } render() { @@ -36,18 +70,16 @@ export class PubkyAuthWidget extends LitElement {
- + ` } _setQr(canvas) { - console.log(canvas, this.authUrl); - QRCode.toCanvas(canvas, this.authUrl, { margin: 2, scale: 8, @@ -63,6 +95,25 @@ export class PubkyAuthWidget extends LitElement { this.open = !this.open } + async _onCallback(response) { + try { + // Check if the response is ok (status code 200-299) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Convert the response to an ArrayBuffer + const arrayBuffer = await response.arrayBuffer(); + + // Create a Uint8Array from the ArrayBuffer + const uint8Array = new Uint8Array(arrayBuffer); + + console.log({ uint8Array }) + } catch (error) { + console.error('Failed to fetch and convert the API response:', error); + } + } + static get styles() { return css` * { @@ -111,7 +162,7 @@ export class PubkyAuthWidget extends LitElement { will-change: height,width; transition-property: height, width; - transition-duration: 200ms; + transition-duration: 80ms; transition-timing-function: ease-in; } @@ -162,6 +213,7 @@ export class PubkyAuthWidget extends LitElement { .card.url { padding: .625rem; justify-content: space-between; + max-width:100%; } .url p { From 52fddd807bd0c64050c25aef31dde689437fac52 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 31 Aug 2024 00:12:04 +0300 Subject: [PATCH 101/125] examples(authz): initial authenticator using keyring crate --- examples/authz/3rd-party-app/index.html | 6 +- .../3rd-party-app/src/pubky-auth-widget.js | 56 +- examples/authz/README.md | 29 + examples/authz/authenticator/Cargo.lock | 1906 +++++++++++++++++ examples/authz/authenticator/Cargo.toml | 13 + examples/authz/authenticator/src/main.rs | 91 + 6 files changed, 2092 insertions(+), 9 deletions(-) create mode 100644 examples/authz/README.md create mode 100644 examples/authz/authenticator/Cargo.lock create mode 100644 examples/authz/authenticator/Cargo.toml create mode 100644 examples/authz/authenticator/src/main.rs diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html index abfe35a..8147b24 100644 --- a/examples/authz/3rd-party-app/index.html +++ b/examples/authz/3rd-party-app/index.html @@ -9,7 +9,11 @@ - + +

Third Party app!

diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js index a01a1bb..09e73ef 100644 --- a/examples/authz/3rd-party-app/src/pubky-auth-widget.js +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -19,6 +19,10 @@ export class PubkyAuthWidget extends LitElement { * https://demo.httprelay.io/link */ relay: { type: String }, + /** + * Capabilities requested or this application encoded as a string. + */ + caps: { type: String }, /** * Widget's state (open or closed) */ @@ -50,10 +54,10 @@ export class PubkyAuthWidget extends LitElement { const channel = Math.random().toString(32).slice(2); callbackUrl.pathname = callbackUrl.pathname + "/" + channel - this.authUrl = `pubky:auth?cb=${callbackUrl.toString()}`; + this.authUrl = `pubkyauth:///?cb=${callbackUrl.toString()}&caps=${this.caps}`; fetch(callbackUrl) - .catch(console.log) + .catch(error => console.error("PubkyAuthWidget: Failed to subscribe to http relay channel", error)) .then(this._onCallback) } @@ -63,17 +67,22 @@ export class PubkyAuthWidget extends LitElement { id="widget" class=${this.open ? "open" : ""} > - +

Scan or copy Pubky auth URL

- +
` @@ -110,7 +119,7 @@ export class PubkyAuthWidget extends LitElement { console.log({ uint8Array }) } catch (error) { - console.error('Failed to fetch and convert the API response:', error); + console.error('PubkyAuthWidget: Failed to read incoming AuthToken', error); } } @@ -124,6 +133,11 @@ export class PubkyAuthWidget extends LitElement { --full-width: 22rem; --full-height: 31rem; --header-height: 3rem; + --closed-width: 3rem; + } + + a { + text-decoration: none; } button { @@ -157,7 +171,7 @@ export class PubkyAuthWidget extends LitElement { -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); - width: 7rem; + width: var(--closed-width); height: var(--header-height); will-change: height,width; @@ -172,13 +186,39 @@ export class PubkyAuthWidget extends LitElement { } .header { - width: 100%; height: var(--header-height); display: flex; justify-content: center; align-items: center; } + #widget + .header .text { + display: none; + font-weight: bold; + } + #widget.open + .header .text { + display: block + } + + #widget.open + .header { + width: var(--full-width); + justify-content: center; + } + + #pubky-icon { + height: 100%; + width: 100%; + } + + #widget.open + #pubky-icon { + width: var(--header-height); + height: 74%; + } + #widget-content{ width: var(--full-width); padding: 0 1rem diff --git a/examples/authz/README.md b/examples/authz/README.md new file mode 100644 index 0000000..905bda6 --- /dev/null +++ b/examples/authz/README.md @@ -0,0 +1,29 @@ +# Pubky Auth Example + +This example shows 3rd party authorization in Pubky. + +It consists of 2 parts: + +1. [3rd party app](./3rd-party-app): A web component showing the how to implement a Pubky Auth widget. +2. [Authenticator CLI](./authenticator): A CLI showing the authenticator (key chain) asking user for consent and generating the AuthToken. + +## Usage + +First you need to be running a local testnet Homeserver, in the root of this repo run + +```bash +cargo run --bin pubky_homeserver -- --testnet +``` + +Run the frontend of the 3rd party app + +```bash +cd ./3rd-party-app +npm start +``` + +Copy the Pubky Auth URL from the frontend. + +Finally run the CLI to paste the Pubky Auth in. + +You should see the frontend reacting by showing the success of authorization and session details. diff --git a/examples/authz/authenticator/Cargo.lock b/examples/authz/authenticator/Cargo.lock new file mode 100644 index 0000000..f2fe8b2 --- /dev/null +++ b/examples/authz/authenticator/Cargo.lock @@ -0,0 +1,1906 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "authenticator" +version = "0.1.0" +dependencies = [ + "anyhow", + "keyring", + "pubky", + "pubky-common", + "rpassword", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +dependencies = [ + "cookie", + "idna 0.5.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "critical-section" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64009896348fc5af4222e9cf7d7d82a95a256c634ebcf61c53e4ea461422242" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b9af47ded4df3067484d7d45758ca2b36bd083bf6d024c2952bbd8af1cdaa4" +dependencies = [ + "byteorder", + "linux-keyutils", + "security-framework", + "windows-sys 0.59.0", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" + +[[package]] +name = "mainline" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" +dependencies = [ + "bytes", + "crc", + "ed25519-dalek", + "flume", + "lru", + "rand", + "serde", + "serde_bencode", + "serde_bytes", + "sha1_smol", + "thiserror", + "tracing", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkarr" +version = "2.2.0" +source = "git+https://github.com/Pubky/pkarr?branch=v3#17975121c809d97dcad907fbb2ffc782e994d270" +dependencies = [ + "base32", + "bytes", + "document-features", + "dyn-clone", + "ed25519-dalek", + "flume", + "futures", + "js-sys", + "lru", + "mainline", + "rand", + "self_cell", + "serde", + "simple-dns", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postcard" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "pubky" +version = "0.1.0" +dependencies = [ + "argon2", + "bytes", + "js-sys", + "pkarr", + "pubky-common", + "reqwest", + "thiserror", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "pubky-common" +version = "0.1.0" +dependencies = [ + "base32", + "blake3", + "crypto_secretbox", + "ed25519-dalek", + "js-sys", + "once_cell", + "pkarr", + "postcard", + "rand", + "serde", + "thiserror", +] + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bencode" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" +dependencies = [ + "serde", + "serde_bytes", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simple-dns" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" +dependencies = [ + "bitflags", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/examples/authz/authenticator/Cargo.toml b/examples/authz/authenticator/Cargo.toml new file mode 100644 index 0000000..8c1e674 --- /dev/null +++ b/examples/authz/authenticator/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "authenticator" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +keyring = { version = "3.2.0", features = ["linux-native", "apple-native", "windows-native"] } +pubky = { version = "0.1.0", path = "../../../pubky" } +pubky-common = { version = "0.1.0", path = "../../../pubky-common" } +rpassword = "7.3.1" + +[workspace] diff --git a/examples/authz/authenticator/src/main.rs b/examples/authz/authenticator/src/main.rs new file mode 100644 index 0000000..828128c --- /dev/null +++ b/examples/authz/authenticator/src/main.rs @@ -0,0 +1,91 @@ +use std::io; + +use keyring::Entry; +use pubky_common::crypto::Keypair; + +const SERVICE_NAME: &str = "pubky"; + +fn main() -> anyhow::Result<()> { + println!("Enter the alias for your keypair in your operating system secure storage:"); + let mut name = String::new(); + io::stdin().read_line(&mut name)?; + name = name.trim_end().to_lowercase(); + + let entry = Entry::new(SERVICE_NAME, &name)?; + + let keypair = match entry.get_secret() { + Ok(secret_key) => { + let secret_key: &[u8; 32] = secret_key + .as_slice() + .try_into() + .expect("Invalid secret_key"); + let keypair = Keypair::from_secret_key(&secret_key); + + println!("\nFound secret_key for Pubky {}", keypair.public_key()); + + keypair + } + Err(error) => { + let keypair = Keypair::random(); + + println!( + "\n{}\nGenerated new Pubky {}", + error.to_string(), + keypair.public_key() + ); + + loop { + println!("\nStore the new Pubky keypair in operating system secure storage?[y/n]"); + let mut choice = String::new(); + io::stdin().read_line(&mut choice)?; + + match choice.as_str() { + "y\n" => { + entry.set_secret(&keypair.secret_key())?; + + break; + } + "n\n" => { + return Ok(()); + } + _ => {} + }; + } + + keypair + } + }; + dbg!(keypair); + + // entry.set_password("topS3cr3tP4$$w0rd")?; + // let password = entry.get_password()?; + // println!("My password is '{}'", password); + // entry.delete_credential()?; + // Ok(()) + + Ok(()) +} + +// fn read_file_content>(path: P) -> io::Result> { +// let mut file = File::open(path)?; +// let mut content = Vec::new(); +// file.read_to_end(&mut content)?; +// Ok(content) +// } + +// fn decrypt_content(content: &[u8], password: &str) -> Result { +// // Create a key and IV (Initialization Vector) from the password +// let key = GenericArray::from_slice(password.as_bytes()); // Example key derivation +// let iv = GenericArray::from_slice(b"unique_iv_16bytes"); // Replace with proper IV derivation +// +// let cipher = Aes256::new(key, iv); +// +// // Decrypt the content using the cipher +// let mut buffer = content.to_vec(); +// cipher +// .decrypt_padded_mut::(&mut buffer) +// .map_err(|_| "Decryption failed")?; +// +// // Convert decrypted bytes to string +// String::from_utf8(buffer).map_err(|_| "Failed to convert decrypted content to string") +// } From 8c56e6033b6669805f273c61c3a6cf80632fa032 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 31 Aug 2024 11:09:16 +0300 Subject: [PATCH 102/125] wip: test keyring cross platforms --- Cargo.lock | 813 ++++++++++++++++++++++- Cargo.toml | 7 +- examples/authz/authenticator/Cargo.toml | 8 +- examples/authz/authenticator/src/main.rs | 151 ++--- 4 files changed, 889 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df600ac..394bb33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -115,6 +126,139 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.81" @@ -135,6 +279,29 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "authenticator" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "dialoguer", + "dirs-next", + "keyring", + "keyring-search", + "pubky", + "pubky-common", + "rpassword", + "serde", + "serde_json", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -322,6 +489,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -340,6 +529,15 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.1.3" @@ -352,6 +550,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "cipher" version = "0.4.4" @@ -365,9 +569,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.11" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -375,9 +579,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.11" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -387,9 +591,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -415,6 +619,28 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -455,6 +681,22 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -572,6 +814,19 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -660,12 +915,82 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -747,6 +1072,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -927,12 +1265,36 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.1.0" @@ -1055,6 +1417,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -1085,6 +1448,33 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyring" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b9af47ded4df3067484d7d45758ca2b36bd083bf6d024c2952bbd8af1cdaa4" +dependencies = [ + "byteorder", + "linux-keyutils", + "security-framework", + "windows-sys 0.59.0", +] + +[[package]] +name = "keyring-search" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fba83ff0a0efb658afeaaa6de89c7abd3ccd34333f5a36d5dae417334fcd488" +dependencies = [ + "byteorder", + "lazy_static", + "linux-keyutils", + "regex", + "secret-service", + "security-framework", + "windows-sys 0.52.0", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1107,6 +1497,22 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "litrs" version = "0.4.1" @@ -1187,6 +1593,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1222,6 +1637,19 @@ dependencies = [ "getrandom", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1232,19 +1660,92 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -1269,6 +1770,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "overload" version = "0.1.1" @@ -1285,6 +1796,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1399,6 +1916,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkarr" version = "2.2.0" @@ -1435,6 +1963,21 @@ dependencies = [ "spki", ] +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -1470,6 +2013,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -1697,6 +2249,27 @@ dependencies = [ "winreg", ] +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1712,6 +2285,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1739,6 +2325,48 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand", + "serde", + "sha2", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.0.4" @@ -1811,6 +2439,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.7" @@ -1869,6 +2508,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1952,6 +2597,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1996,6 +2647,19 @@ dependencies = [ "crossbeam-queue", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.62" @@ -2285,6 +2949,17 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2306,6 +2981,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2476,6 +3157,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2616,8 +3306,117 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index d1fbeb9..8514809 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] -members = ["pubky","pubky-*"] +members = [ + "pubky", + "pubky-*", + + "examples/authz/authenticator" +] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/examples/authz/authenticator/Cargo.toml b/examples/authz/authenticator/Cargo.toml index 8c1e674..5eaca23 100644 --- a/examples/authz/authenticator/Cargo.toml +++ b/examples/authz/authenticator/Cargo.toml @@ -5,9 +5,13 @@ edition = "2021" [dependencies] anyhow = "1.0.86" +clap = "4.5.16" +dialoguer = "0.11.0" +dirs-next = "2.0.0" keyring = { version = "3.2.0", features = ["linux-native", "apple-native", "windows-native"] } +keyring-search = "1.2.1" pubky = { version = "0.1.0", path = "../../../pubky" } pubky-common = { version = "0.1.0", path = "../../../pubky-common" } rpassword = "7.3.1" - -[workspace] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/examples/authz/authenticator/src/main.rs b/examples/authz/authenticator/src/main.rs index 828128c..cc36c1f 100644 --- a/examples/authz/authenticator/src/main.rs +++ b/examples/authz/authenticator/src/main.rs @@ -1,91 +1,82 @@ -use std::io; +// use std::io; -use keyring::Entry; -use pubky_common::crypto::Keypair; +use anyhow::Result; +// use clap::{App, Arg, SubCommand}; +// use dialoguer::{Input, Select}; + +use keyring_search::{Limit, List, Search}; + +// use keyring::Entry; +// use pubky_common::crypto::Keypair; const SERVICE_NAME: &str = "pubky"; -fn main() -> anyhow::Result<()> { - println!("Enter the alias for your keypair in your operating system secure storage:"); - let mut name = String::new(); - io::stdin().read_line(&mut name)?; - name = name.trim_end().to_lowercase(); +fn main() -> Result<()> { + let result = Search::new().unwrap().by_service(SERVICE_NAME).unwrap(); - let entry = Entry::new(SERVICE_NAME, &name)?; + // let list: Vec<_> = result + // .values() + // .map(|v| { + // dbg!(&v); + // v.get("acct") + // }) + // .filter(|acc| acc.is_some()) + // .collect(); - let keypair = match entry.get_secret() { - Ok(secret_key) => { - let secret_key: &[u8; 32] = secret_key - .as_slice() - .try_into() - .expect("Invalid secret_key"); - let keypair = Keypair::from_secret_key(&secret_key); + let list = List::list_credentials(&Search::new().unwrap().by_service(SERVICE_NAME), Limit::All); - println!("\nFound secret_key for Pubky {}", keypair.public_key()); + dbg!(list); - keypair - } - Err(error) => { - let keypair = Keypair::random(); - - println!( - "\n{}\nGenerated new Pubky {}", - error.to_string(), - keypair.public_key() - ); - - loop { - println!("\nStore the new Pubky keypair in operating system secure storage?[y/n]"); - let mut choice = String::new(); - io::stdin().read_line(&mut choice)?; - - match choice.as_str() { - "y\n" => { - entry.set_secret(&keypair.secret_key())?; - - break; - } - "n\n" => { - return Ok(()); - } - _ => {} - }; - } - - keypair - } - }; - dbg!(keypair); - - // entry.set_password("topS3cr3tP4$$w0rd")?; - // let password = entry.get_password()?; - // println!("My password is '{}'", password); - // entry.delete_credential()?; - // Ok(()) + // println!("Enter the alias for your keypair in your operating system secure storage:"); + // let mut name = String::new(); + // io::stdin().read_line(&mut name)?; + // name = name.trim_end().to_lowercase(); + // + // let entry = Entry::new(SERVICE_NAME, &name)?; + // + // let keypair = match entry.get_secret() { + // Ok(secret_key) => { + // let secret_key: &[u8; 32] = secret_key + // .as_slice() + // .try_into() + // .expect("Invalid secret_key"); + // let keypair = Keypair::from_secret_key(&secret_key); + // + // println!("\nFound secret_key for Pubky {}", keypair.public_key()); + // + // keypair + // } + // Err(error) => { + // let keypair = Keypair::random(); + // + // println!( + // "\n{}\nGenerated new Pubky {}", + // error.to_string(), + // keypair.public_key() + // ); + // + // loop { + // println!("\nStore the new Pubky keypair in operating system secure storage?[y/n]"); + // let mut choice = String::new(); + // io::stdin().read_line(&mut choice)?; + // + // match choice.as_str() { + // "y\n" => { + // entry.set_secret(&keypair.secret_key())?; + // + // break; + // } + // "n\n" => { + // return Ok(()); + // } + // _ => {} + // }; + // } + // + // keypair + // } + // }; + // dbg!(keypair); Ok(()) } - -// fn read_file_content>(path: P) -> io::Result> { -// let mut file = File::open(path)?; -// let mut content = Vec::new(); -// file.read_to_end(&mut content)?; -// Ok(content) -// } - -// fn decrypt_content(content: &[u8], password: &str) -> Result { -// // Create a key and IV (Initialization Vector) from the password -// let key = GenericArray::from_slice(password.as_bytes()); // Example key derivation -// let iv = GenericArray::from_slice(b"unique_iv_16bytes"); // Replace with proper IV derivation -// -// let cipher = Aes256::new(key, iv); -// -// // Decrypt the content using the cipher -// let mut buffer = content.to_vec(); -// cipher -// .decrypt_padded_mut::(&mut buffer) -// .map_err(|_| "Decryption failed")?; -// -// // Convert decrypted bytes to string -// String::from_utf8(buffer).map_err(|_| "Failed to convert decrypted content to string") -// } From de38987d96908b030e9b4ebd056280b69a004c32 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 31 Aug 2024 17:29:32 +0300 Subject: [PATCH 103/125] refactor(pubky): move recovery_file code to pubky_common --- Cargo.lock | 772 +----------------- examples/authz/authenticator/Cargo.toml | 9 +- examples/authz/authenticator/src/main.rs | 98 +-- pubky-common/Cargo.toml | 1 + pubky-common/src/lib.rs | 1 + .../src}/recovery_file.rs | 37 +- pubky/Cargo.toml | 1 - pubky/src/error.rs | 15 +- pubky/src/native.rs | 18 +- pubky/src/shared/mod.rs | 1 - pubky/src/wasm.rs | 12 +- pubky/src/wasm/http.rs | 2 - pubky/src/wasm/keys.rs | 2 +- 13 files changed, 76 insertions(+), 893 deletions(-) rename {pubky/src/shared => pubky-common/src}/recovery_file.rs (67%) diff --git a/Cargo.lock b/Cargo.lock index 394bb33..29a7d4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,17 +27,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -126,139 +115,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" -[[package]] -name = "async-broadcast" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-signal" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.81" @@ -279,27 +135,16 @@ dependencies = [ "critical-section", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "authenticator" version = "0.1.0" dependencies = [ "anyhow", "clap", - "dialoguer", - "dirs-next", - "keyring", - "keyring-search", "pubky", "pubky-common", "rpassword", - "serde", - "serde_json", + "url", ] [[package]] @@ -489,28 +334,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -529,15 +352,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - [[package]] name = "cc" version = "1.1.3" @@ -550,12 +364,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "cipher" version = "0.4.4" @@ -619,28 +427,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -681,22 +467,6 @@ dependencies = [ "url", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.12" @@ -814,19 +584,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "dialoguer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" -dependencies = [ - "console", - "shell-words", - "tempfile", - "thiserror", - "zeroize", -] - [[package]] name = "digest" version = "0.10.7" @@ -915,82 +672,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "endi" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" - -[[package]] -name = "enumflags2" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1072,19 +759,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" -[[package]] -name = "futures-lite" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.30" @@ -1265,36 +939,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" version = "1.1.0" @@ -1417,7 +1067,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "block-padding", "generic-array", ] @@ -1448,33 +1097,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keyring" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b9af47ded4df3067484d7d45758ca2b36bd083bf6d024c2952bbd8af1cdaa4" -dependencies = [ - "byteorder", - "linux-keyutils", - "security-framework", - "windows-sys 0.59.0", -] - -[[package]] -name = "keyring-search" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fba83ff0a0efb658afeaaa6de89c7abd3ccd34333f5a36d5dae417334fcd488" -dependencies = [ - "byteorder", - "lazy_static", - "linux-keyutils", - "regex", - "secret-service", - "security-framework", - "windows-sys 0.52.0", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1497,22 +1119,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-keyutils" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - [[package]] name = "litrs" version = "0.4.1" @@ -1593,15 +1199,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.17" @@ -1637,19 +1234,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1660,92 +1244,19 @@ dependencies = [ "winapi", ] -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -1770,16 +1281,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "overload" version = "0.1.1" @@ -1796,12 +1297,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - [[package]] name = "parking_lot" version = "0.12.3" @@ -1916,17 +1411,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkarr" version = "2.2.0" @@ -1963,21 +1447,6 @@ dependencies = [ "spki", ] -[[package]] -name = "polling" -version = "3.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.4.0", - "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -2013,15 +1482,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "proc-macro-crate" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro2" version = "1.0.86" @@ -2041,7 +1501,6 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" name = "pubky" version = "0.1.0" dependencies = [ - "argon2", "bytes", "js-sys", "pkarr", @@ -2059,6 +1518,7 @@ dependencies = [ name = "pubky-common" version = "0.1.0" dependencies = [ + "argon2", "base32", "blake3", "crypto_secretbox", @@ -2285,19 +1745,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - [[package]] name = "rustversion" version = "1.0.17" @@ -2325,48 +1772,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "secret-service" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" -dependencies = [ - "aes", - "cbc", - "futures-util", - "generic-array", - "hkdf", - "num", - "once_cell", - "rand", - "serde", - "sha2", - "zbus", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "self_cell" version = "1.0.4" @@ -2439,17 +1844,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_spanned" version = "0.6.7" @@ -2508,12 +1902,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2597,12 +1985,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -2647,19 +2029,6 @@ dependencies = [ "crossbeam-queue", ] -[[package]] -name = "tempfile" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" -dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - [[package]] name = "thiserror" version = "1.0.62" @@ -2949,17 +2318,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2981,12 +2339,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" - [[package]] name = "universal-hash" version = "0.5.1" @@ -3157,15 +2509,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -3306,117 +2649,8 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "zbus" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" -dependencies = [ - "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "ordered-stream", - "rand", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zvariant" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/examples/authz/authenticator/Cargo.toml b/examples/authz/authenticator/Cargo.toml index 5eaca23..2938b6d 100644 --- a/examples/authz/authenticator/Cargo.toml +++ b/examples/authz/authenticator/Cargo.toml @@ -5,13 +5,8 @@ edition = "2021" [dependencies] anyhow = "1.0.86" -clap = "4.5.16" -dialoguer = "0.11.0" -dirs-next = "2.0.0" -keyring = { version = "3.2.0", features = ["linux-native", "apple-native", "windows-native"] } -keyring-search = "1.2.1" +clap = { version = "4.5.16", features = ["derive"] } pubky = { version = "0.1.0", path = "../../../pubky" } pubky-common = { version = "0.1.0", path = "../../../pubky-common" } rpassword = "7.3.1" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +url = "2.5.2" diff --git a/examples/authz/authenticator/src/main.rs b/examples/authz/authenticator/src/main.rs index cc36c1f..2643d7d 100644 --- a/examples/authz/authenticator/src/main.rs +++ b/examples/authz/authenticator/src/main.rs @@ -1,82 +1,40 @@ -// use std::io; - use anyhow::Result; -// use clap::{App, Arg, SubCommand}; -// use dialoguer::{Input, Select}; +use clap::Parser; +use std::path::PathBuf; +use url::Url; -use keyring_search::{Limit, List, Search}; +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Cli { + /// Pubky Auth url + url: Url, -// use keyring::Entry; -// use pubky_common::crypto::Keypair; - -const SERVICE_NAME: &str = "pubky"; + /// Path to a recovery_file of the Pubky you want to sign in with + // #[arg(short, long, value_name = "FILE")] + recovery_file: PathBuf, + // /// Mutable data public key. + // public_key: String, +} fn main() -> Result<()> { - let result = Search::new().unwrap().by_service(SERVICE_NAME).unwrap(); + let cli = Cli::parse(); - // let list: Vec<_> = result - // .values() - // .map(|v| { - // dbg!(&v); - // v.get("acct") - // }) - // .filter(|acc| acc.is_some()) - // .collect(); + let url = cli.url; + dbg!(url); - let list = List::list_credentials(&Search::new().unwrap().by_service(SERVICE_NAME), Limit::All); + let recovery_file = std::fs::read(&cli.recovery_file)?; + println!("Successfully opened recovery file"); - dbg!(list); + // // println!("Enter Pubky Auth URL to start the consent form:"); + // // let pubky_auth_url = rl.readline("> ")?; + // // dbg!(pubky_auth_url); - // println!("Enter the alias for your keypair in your operating system secure storage:"); - // let mut name = String::new(); - // io::stdin().read_line(&mut name)?; - // name = name.trim_end().to_lowercase(); - // - // let entry = Entry::new(SERVICE_NAME, &name)?; - // - // let keypair = match entry.get_secret() { - // Ok(secret_key) => { - // let secret_key: &[u8; 32] = secret_key - // .as_slice() - // .try_into() - // .expect("Invalid secret_key"); - // let keypair = Keypair::from_secret_key(&secret_key); - // - // println!("\nFound secret_key for Pubky {}", keypair.public_key()); - // - // keypair - // } - // Err(error) => { - // let keypair = Keypair::random(); - // - // println!( - // "\n{}\nGenerated new Pubky {}", - // error.to_string(), - // keypair.public_key() - // ); - // - // loop { - // println!("\nStore the new Pubky keypair in operating system secure storage?[y/n]"); - // let mut choice = String::new(); - // io::stdin().read_line(&mut choice)?; - // - // match choice.as_str() { - // "y\n" => { - // entry.set_secret(&keypair.secret_key())?; - // - // break; - // } - // "n\n" => { - // return Ok(()); - // } - // _ => {} - // }; - // } - // - // keypair - // } - // }; - // dbg!(keypair); + println!("Enter your recovery_file's passphrase to confirm:"); + let passphrase = rpassword::read_password()?; + + let keypair = pubky_common::recovery_file::decrypt_recovery_file(&recovery_file, &passphrase)?; + + println!("Successfully decrypted recovery file..."); Ok(()) } diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 675ec65..9676fba 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -15,6 +15,7 @@ rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } crypto_secretbox = { version = "0.1.1", features = ["std"] } +argon2 = { version = "0.5.3", features = ["std"] } serde = { workspace = true, optional = true } diff --git a/pubky-common/src/lib.rs b/pubky-common/src/lib.rs index 5234c51..cfb56f2 100644 --- a/pubky-common/src/lib.rs +++ b/pubky-common/src/lib.rs @@ -2,5 +2,6 @@ pub mod auth; pub mod capabilities; pub mod crypto; pub mod namespaces; +pub mod recovery_file; pub mod session; pub mod timestamp; diff --git a/pubky/src/shared/recovery_file.rs b/pubky-common/src/recovery_file.rs similarity index 67% rename from pubky/src/shared/recovery_file.rs rename to pubky-common/src/recovery_file.rs index 4bcbc27..0a2f9b4 100644 --- a/pubky/src/shared/recovery_file.rs +++ b/pubky-common/src/recovery_file.rs @@ -1,13 +1,12 @@ use argon2::Argon2; use pkarr::Keypair; -use pubky_common::crypto::{decrypt, encrypt}; -use crate::error::{Error, Result}; +use crate::crypto::{decrypt, encrypt}; static SPEC_NAME: &str = "recovery"; static SPEC_LINE: &str = "pubky.org/recovery"; -pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { +pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; let newline_index = recovery_file @@ -39,7 +38,7 @@ pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result Result> { +pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result, Error> { let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?; let secret_key = keypair.secret_key(); @@ -54,7 +53,7 @@ pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result Result<[u8; 32]> { +fn recovery_file_encryption_key_from_passphrase(passphrase: &str) -> Result<[u8; 32], Error> { let argon2id = Argon2::default(); let mut out = [0; 32]; @@ -64,19 +63,39 @@ fn recovery_file_encryption_key_from_passphrase(passphrase: &str) -> Result<[u8; Ok(out) } +#[derive(thiserror::Error, Debug)] +pub enum Error { + // === Recovery file == + #[error("Recovery file should start with a spec line, followed by a new line character")] + RecoveryFileMissingSpecLine, + + #[error("Recovery file should start with a spec line, followed by a new line character")] + RecoveryFileVersionNotSupported, + + #[error("Recovery file should contain an encrypted secret key after the new line character")] + RecoverFileMissingEncryptedSecretKey, + + #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")] + RecoverFileInvalidSecretKeyLength(usize), + + #[error(transparent)] + Argon(#[from] argon2::Error), + + #[error(transparent)] + Crypto(#[from] crate::crypto::Error), +} + #[cfg(test)] mod tests { use super::*; - use crate::PubkyClient; - #[test] fn encrypt_decrypt_recovery_file() { let passphrase = "very secure password"; let keypair = Keypair::random(); - let recovery_file = PubkyClient::create_recovery_file(&keypair, passphrase).unwrap(); - let recovered = PubkyClient::decrypt_recovery_file(&recovery_file, passphrase).unwrap(); + let recovery_file = create_recovery_file(&keypair, passphrase).unwrap(); + let recovered = decrypt_recovery_file(&recovery_file, passphrase).unwrap(); assert_eq!(recovered.public_key(), keypair.public_key()); } diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 90039ee..29c2c9b 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -17,7 +17,6 @@ url = "2.5.2" bytes = "^1.7.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } -argon2 = { version = "0.5.3", features = ["std"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pkarr = { workspace = true, features = ["async"] } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 40eca3b..cb51053 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -15,19 +15,6 @@ pub enum Error { #[error("Could not resolve endpoint for {0}")] ResolveEndpoint(String), - // === Recovery file == - #[error("Recovery file should start with a spec line, followed by a new line character")] - RecoveryFileMissingSpecLine, - - #[error("Recovery file should start with a spec line, followed by a new line character")] - RecoveryFileVersionNotSupported, - - #[error("Recovery file should contain an encrypted secret key after the new line character")] - RecoverFileMissingEncryptedSecretKey, - - #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")] - RecoverFileInvalidSecretKeyLength(usize), - #[error("Could not convert the passed type into a Url")] InvalidUrl, @@ -51,7 +38,7 @@ pub enum Error { Crypto(#[from] pubky_common::crypto::Error), #[error(transparent)] - Argon(#[from] argon2::Error), + RecoveryFile(#[from] pubky_common::recovery_file::Error), } #[cfg(target_arch = "wasm32")] diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 91adb02..ecae6b0 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -6,18 +6,14 @@ use ::pkarr::{ }; use bytes::Bytes; use pkarr::Keypair; -use pubky_common::session::Session; +use pubky_common::{ + recovery_file::{create_recovery_file, decrypt_recovery_file}, + session::Session, +}; use reqwest::{RequestBuilder, Response}; use url::Url; -use crate::{ - error::Result, - shared::{ - list_builder::ListBuilder, - recovery_file::{create_recovery_file, decrypt_recovery_file}, - }, - PubkyClient, -}; +use crate::{error::Result, shared::list_builder::ListBuilder, PubkyClient}; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -151,12 +147,12 @@ impl PubkyClient { /// Create a recovery file of the `keypair`, containing the secret key encrypted /// using the `passphrase`. pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { - create_recovery_file(keypair, passphrase) + Ok(create_recovery_file(keypair, passphrase)?) } /// Recover a keypair from a recovery file by decrypting the secret key using `passphrase`. pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { - decrypt_recovery_file(recovery_file, passphrase) + Ok(decrypt_recovery_file(recovery_file, passphrase)?) } } diff --git a/pubky/src/shared/mod.rs b/pubky/src/shared/mod.rs index 550cc6e..67b456f 100644 --- a/pubky/src/shared/mod.rs +++ b/pubky/src/shared/mod.rs @@ -2,4 +2,3 @@ pub mod auth; pub mod list_builder; pub mod pkarr; pub mod public; -pub mod recovery_file; diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 536949f..017bf61 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -6,13 +6,9 @@ use std::{ use js_sys::{Array, Uint8Array}; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; -use reqwest::{IntoUrl, Method, RequestBuilder, Response}; -use url::Url; +use pubky_common::recovery_file::{create_recovery_file, decrypt_recovery_file}; -use crate::{ - shared::recovery_file::{create_recovery_file, decrypt_recovery_file}, - PubkyClient, -}; +use crate::{error::Error, PubkyClient}; mod http; mod keys; @@ -62,7 +58,7 @@ impl PubkyClient { ) -> Result { create_recovery_file(keypair.as_inner(), passphrase) .map(|b| b.as_slice().into()) - .map_err(|e| e.into()) + .map_err(|e| Error::from(e).into()) } /// Create a recovery file of the `keypair`, containing the secret key encrypted @@ -74,7 +70,7 @@ impl PubkyClient { ) -> Result { decrypt_recovery_file(recovery_file, passphrase) .map(Keypair::from) - .map_err(|e| e.into()) + .map_err(|e| Error::from(e).into()) } /// Set Pkarr relays used for publishing and resolving Pkarr packets. diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs index d89d4ce..a845794 100644 --- a/pubky/src/wasm/http.rs +++ b/pubky/src/wasm/http.rs @@ -3,8 +3,6 @@ use crate::PubkyClient; use reqwest::{Method, RequestBuilder, Response}; use url::Url; -use ::pkarr::PublicKey; - impl PubkyClient { pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { let mut request = self.http.request(method, url).fetch_credentials_include(); diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 12ecdd7..c3454ac 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -21,7 +21,7 @@ impl Keypair { } let len = secret_key.byte_length(); - if (len != 32) { + if len != 32 { return Err(format!("Expected secret_key to be 32 bytes, got {len}"))?; } From 915ae66f1a22c49a101fbc28a563adc6428653ca Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 1 Sep 2024 16:47:22 +0300 Subject: [PATCH 104/125] feat(js): add window.pubky and move static methods out of PubkyClient --- examples/authz/3rd-party-app/index.html | 5 +- .../authz/3rd-party-app/package-lock.json | 613 +++++++++++++++++- examples/authz/3rd-party-app/package.json | 1 + pubky/pkg/test/recovery.js | 6 +- pubky/src/bin/patch.mjs | 20 +- pubky/src/wasm.rs | 29 +- pubky/src/wasm/recovery_file.rs | 24 + 7 files changed, 631 insertions(+), 67 deletions(-) create mode 100644 pubky/src/wasm/recovery_file.rs diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html index 8147b24..ef83865 100644 --- a/examples/authz/3rd-party-app/index.html +++ b/examples/authz/3rd-party-app/index.html @@ -6,12 +6,15 @@ Vite + Lit + diff --git a/examples/authz/3rd-party-app/package-lock.json b/examples/authz/3rd-party-app/package-lock.json index 5ce8a32..98ba42f 100644 --- a/examples/authz/3rd-party-app/package-lock.json +++ b/examples/authz/3rd-party-app/package-lock.json @@ -8,6 +8,7 @@ "name": "pubky-auth-3rd-party", "version": "0.0.0", "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", "lit": "^3.2.0", "qrcode": "^1.5.4" }, @@ -15,6 +16,81 @@ "vite": "^5.4.2" } }, + "../../../pubky/pkg": { + "name": "@synonymdev/pubky", + "version": "0.1.14", + "license": "MIT", + "devDependencies": { + "browser-resolve": "^2.0.0", + "esmify": "^2.1.1", + "tape": "^5.8.1", + "tape-run": "^11.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", @@ -31,6 +107,294 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", @@ -44,10 +408,36 @@ "@lit-labs/ssr-dom-shim": "^1.2.0" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", + "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", + "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", - "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", + "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", "cpu": [ "arm64" ], @@ -57,6 +447,179 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", + "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", + "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", + "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", + "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", + "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", + "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", + "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", + "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", + "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", + "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", + "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", + "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", + "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@synonymdev/pubky": { + "resolved": "../../../pubky/pkg", + "link": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -335,9 +898,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.42", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.42.tgz", + "integrity": "sha512-hywKUQB9Ra4dR1mGhldy5Aj1X3MWDSIA1cEi+Uy0CjheLvP6Ual5RlwMCh8i/X121yEDLDIKBsrCQ8ba3FDMfQ==", "dev": true, "funding": [ { @@ -392,9 +955,9 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "node_modules/rollup": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", - "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", + "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -407,22 +970,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.0", - "@rollup/rollup-android-arm64": "4.21.0", - "@rollup/rollup-darwin-arm64": "4.21.0", - "@rollup/rollup-darwin-x64": "4.21.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", - "@rollup/rollup-linux-arm-musleabihf": "4.21.0", - "@rollup/rollup-linux-arm64-gnu": "4.21.0", - "@rollup/rollup-linux-arm64-musl": "4.21.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", - "@rollup/rollup-linux-riscv64-gnu": "4.21.0", - "@rollup/rollup-linux-s390x-gnu": "4.21.0", - "@rollup/rollup-linux-x64-gnu": "4.21.0", - "@rollup/rollup-linux-x64-musl": "4.21.0", - "@rollup/rollup-win32-arm64-msvc": "4.21.0", - "@rollup/rollup-win32-ia32-msvc": "4.21.0", - "@rollup/rollup-win32-x64-msvc": "4.21.0", + "@rollup/rollup-android-arm-eabi": "4.21.2", + "@rollup/rollup-android-arm64": "4.21.2", + "@rollup/rollup-darwin-arm64": "4.21.2", + "@rollup/rollup-darwin-x64": "4.21.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", + "@rollup/rollup-linux-arm-musleabihf": "4.21.2", + "@rollup/rollup-linux-arm64-gnu": "4.21.2", + "@rollup/rollup-linux-arm64-musl": "4.21.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", + "@rollup/rollup-linux-riscv64-gnu": "4.21.2", + "@rollup/rollup-linux-s390x-gnu": "4.21.2", + "@rollup/rollup-linux-x64-gnu": "4.21.2", + "@rollup/rollup-linux-x64-musl": "4.21.2", + "@rollup/rollup-win32-arm64-msvc": "4.21.2", + "@rollup/rollup-win32-ia32-msvc": "4.21.2", + "@rollup/rollup-win32-x64-msvc": "4.21.2", "fsevents": "~2.3.2" } }, diff --git a/examples/authz/3rd-party-app/package.json b/examples/authz/3rd-party-app/package.json index 11bb8b8..8ccad30 100644 --- a/examples/authz/3rd-party-app/package.json +++ b/examples/authz/3rd-party-app/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", "lit": "^3.2.0", "qrcode": "^1.5.4" }, diff --git a/pubky/pkg/test/recovery.js b/pubky/pkg/test/recovery.js index cf05160..0c033e5 100644 --- a/pubky/pkg/test/recovery.js +++ b/pubky/pkg/test/recovery.js @@ -1,11 +1,11 @@ import test from 'tape' -import { PubkyClient, Keypair } from '../index.cjs' +import { Keypair, createRecoveryFile, decryptRecoveryFile } from '../index.cjs' test('recovery', async (t) => { const keypair = Keypair.random(); - const recoveryFile = PubkyClient.createRecoveryFile(keypair, 'very secure password'); + const recoveryFile = createRecoveryFile(keypair, 'very secure password'); t.is(recoveryFile.length, 91) t.deepEqual( @@ -13,7 +13,7 @@ test('recovery', async (t) => { [112, 117, 98, 107, 121, 46, 111, 114, 103, 47, 114, 101, 99, 111, 118, 101, 114, 121, 10] ) - const recovered = PubkyClient.decryptRecoveryFile(recoveryFile, 'very secure password') + const recovered = decryptRecoveryFile(recoveryFile, 'very secure password') t.is(recovered.publicKey().z32(), keypair.publicKey().z32()) }) diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs index 0e9ebe9..a8ed503 100644 --- a/pubky/src/bin/patch.mjs +++ b/pubky/src/bin/patch.mjs @@ -20,17 +20,15 @@ const patched = content .replace("require(`util`)", "globalThis") // attach to `imports` instead of module.exports .replace("= module.exports", "= imports") - - // add suffix Class - .replace(/\nclass (.*?) \{/g, "\nclass $1Class {") - .replace(/\nmodule\.exports\.(.*?) = (.*?);/g, "\nexport const $1 = imports.$1 = $1Class") - - // quick and dirty fix for a bug caused by the previous replace - .replace(/__wasmClass/g, "wasm") - - .replace(/\nmodule\.exports\.(.*?)\s+/g, "\nexport const $1 = imports.$1 ") + // Export classes + .replace(/\nclass (.*?) \{/g, "\n export class $1 {") + // Export functions + .replace(/\nmodule.exports.(.*?) = function/g, "\nimports.$1 = $1;\nexport function $1") + // Add exports to 'imports' + .replace(/\nmodule\.exports\.(.*?)\s+/g, "\nimports.$1") + // Export default .replace(/$/, 'export default imports') - // inline bytes Uint8Array + // inline wasm bytes .replace( /\nconst path.*\nconst bytes.*\n/, ` @@ -56,7 +54,7 @@ const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `. `, ); -await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched); +await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched + "\nglobalThis['pubky'] = imports"); // Move outside of nodejs diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 017bf61..047f2ac 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -6,13 +6,12 @@ use std::{ use js_sys::{Array, Uint8Array}; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; -use pubky_common::recovery_file::{create_recovery_file, decrypt_recovery_file}; - -use crate::{error::Error, PubkyClient}; +use crate::PubkyClient; mod http; mod keys; mod pkarr; +mod recovery_file; mod session; use keys::{Keypair, PublicKey}; @@ -49,30 +48,6 @@ impl PubkyClient { } } - /// Create a recovery file of the `keypair`, containing the secret key encrypted - /// using the `passphrase`. - #[wasm_bindgen(js_name = "createRecoveryFile")] - pub fn create_recovery_file( - keypair: &Keypair, - passphrase: &str, - ) -> Result { - create_recovery_file(keypair.as_inner(), passphrase) - .map(|b| b.as_slice().into()) - .map_err(|e| Error::from(e).into()) - } - - /// Create a recovery file of the `keypair`, containing the secret key encrypted - /// using the `passphrase`. - #[wasm_bindgen(js_name = "decryptRecoveryFile")] - pub fn decrypt_recovery_file( - recovery_file: &[u8], - passphrase: &str, - ) -> Result { - decrypt_recovery_file(recovery_file, passphrase) - .map(Keypair::from) - .map_err(|e| Error::from(e).into()) - } - /// Set Pkarr relays used for publishing and resolving Pkarr packets. /// /// By default, [PubkyClient] will use `["https://relay.pkarr.org"]` diff --git a/pubky/src/wasm/recovery_file.rs b/pubky/src/wasm/recovery_file.rs new file mode 100644 index 0000000..7b85178 --- /dev/null +++ b/pubky/src/wasm/recovery_file.rs @@ -0,0 +1,24 @@ +use js_sys::Uint8Array; +use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; + +use crate::error::Error; + +use super::keys::Keypair; + +/// Create a recovery file of the `keypair`, containing the secret key encrypted +/// using the `passphrase`. +#[wasm_bindgen(js_name = "createRecoveryFile")] +pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result { + pubky_common::recovery_file::create_recovery_file(keypair.as_inner(), passphrase) + .map(|b| b.as_slice().into()) + .map_err(|e| Error::from(e).into()) +} + +/// Create a recovery file of the `keypair`, containing the secret key encrypted +/// using the `passphrase`. +#[wasm_bindgen(js_name = "decryptRecoveryFile")] +pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { + pubky_common::recovery_file::decrypt_recovery_file(recovery_file, passphrase) + .map(Keypair::from) + .map_err(|e| Error::from(e).into()) +} From 5d9be5c0f09104de3023f895b4381122287e81e5 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 1 Sep 2024 18:17:34 +0300 Subject: [PATCH 105/125] feat(js): add randomBytes() and hash() functions --- .../3rd-party-app/src/pubky-auth-widget.js | 28 +++++++++-- pubky-common/src/crypto.rs | 7 +-- pubky-homeserver/src/routes/auth.rs | 2 +- pubky/pkg/README.md | 50 +++++++++++++------ pubky/src/wasm.rs | 1 + pubky/src/wasm/crypto.rs | 17 +++++++ 6 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 pubky/src/wasm/crypto.rs diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js index 09e73ef..4535840 100644 --- a/examples/authz/3rd-party-app/src/pubky-auth-widget.js +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -33,9 +33,16 @@ export class PubkyAuthWidget extends LitElement { canvasRef = createRef(); constructor() { - // TODO: show error if the PubkyClient is not available! + if (!window.pubky) { + throw new Error("window.pubky is unavailable, make sure to import `@synonymdev/pubky` before this web component.") + } + super() + this.open = false; + + this.secret = window.pubky.randomBytes(32) + this.channelId = base64url(window.pubky.hash(this.secret)) } connectedCallback() { @@ -51,10 +58,10 @@ export class PubkyAuthWidget extends LitElement { ) : DEFAULT_HTTP_RELAY - const channel = Math.random().toString(32).slice(2); - callbackUrl.pathname = callbackUrl.pathname + "/" + channel + callbackUrl.pathname = callbackUrl.pathname + "/" + this.channelId - this.authUrl = `pubkyauth:///?cb=${callbackUrl.toString()}&caps=${this.caps}`; + this.authUrl = `pubkyauth://${callbackUrl.hostname + callbackUrl.pathname}?capabilities=${this.caps}&secret=${base64url(this.secret)}`; + console.log({ url: this.authUrl }); fetch(callbackUrl) .catch(error => console.error("PubkyAuthWidget: Failed to subscribe to http relay channel", error)) @@ -278,3 +285,16 @@ export class PubkyAuthWidget extends LitElement { } window.customElements.define('pubky-auth-widget', PubkyAuthWidget) + +function base64url(input) { + // Convert Uint8Array to a binary string + let binaryString = ''; + for (let i = 0; i < input.length; i++) { + binaryString += String.fromCharCode(input[i]); + } + + return btoa(binaryString) + .replace(/\+/g, '-') // Replace + with - + .replace(/\//g, '_') // Replace / with _ + .replace(/=+$/, '') // Remove padding (i.e., =) +} diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index a7adea5..82240d1 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -19,14 +19,15 @@ pub fn random_hash() -> Hash { Hash::from_bytes(rng.gen()) } -pub fn random_bytes() -> [u8; N] { +pub fn random_bytes(size: usize) -> Vec { let mut rng = rand::thread_rng(); - let mut arr = [0u8; N]; + let mut arr = vec![0u8; size]; #[allow(clippy::needless_range_loop)] - for i in 0..N { + for i in 0..size { arr[i] = rng.gen(); } + arr } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 9bfe5f8..a832e17 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -111,7 +111,7 @@ pub async fn signin( )?; } - let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); + let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes(16)); state.db.tables.sessions.put( &mut wtxn, diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 3fcbcd1..6e33389 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -67,22 +67,6 @@ await client.delete(url); let client = new PubkyClient() ``` -#### createRecoveryFile -```js -let recoveryFile = PubkyClient.createRecoveryFile(keypair, passphrase) -``` -- keypair: An instance of [Keypair](#keypair). -- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). -- Returns: A recovery file with a spec line and an encrypted secret key. - -#### createRecoveryFile -```js -let keypair = PubkyClient.decryptRecoveryfile(recoveryFile, passphrase) -``` -- recoveryFile: An instance of Uint8Array containing the recovery file blob. -- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). -- Returns: An instance of [Keypair](#keypair). - #### signup ```js await client.signup(keypair, homeserver) @@ -172,6 +156,40 @@ let pubky = publicKey.z32(); ``` Returns: The z-base-32 encoded string representation of the PublicKey. +### Helper functions + +#### createRecoveryFile +```js +let recoveryFile = createRecoveryFile(keypair, passphrase) +``` +- keypair: An instance of [Keypair](#keypair). +- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). +- Returns: A recovery file with a spec line and an encrypted secret key. + +#### createRecoveryFile +```js +let keypair = decryptRecoveryfile(recoveryFile, passphrase) +``` +- recoveryFile: An instance of Uint8Array containing the recovery file blob. +- passphrase: A utf-8 string [passphrase](https://www.useapassphrase.com/). +- Returns: An instance of [Keypair](#keypair). + +#### randomBytes +```js +let bytes = randomBytes(size) +``` +Generates random bytes + +- size: The number of random bytes to be generated + +### hash +```js +let hash = hash(input) +``` +Creates a Blake3 hash of the input. + +- input: A Uint8Array input to be hashed. + ## Test and Development For test and development, you can run a local homeserver in a test network. diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 047f2ac..8cce6f1 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -8,6 +8,7 @@ use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; use crate::PubkyClient; +mod crypto; mod http; mod keys; mod pkarr; diff --git a/pubky/src/wasm/crypto.rs b/pubky/src/wasm/crypto.rs new file mode 100644 index 0000000..8d7307a --- /dev/null +++ b/pubky/src/wasm/crypto.rs @@ -0,0 +1,17 @@ +use js_sys::Uint8Array; +use wasm_bindgen::prelude::wasm_bindgen; + +/// Generate random bytes. +#[wasm_bindgen(js_name = "randomBytes")] +pub fn random_bytes(size: usize) -> Uint8Array { + pubky_common::crypto::random_bytes(size).as_slice().into() +} + +/// Pubky hash function (blake3) +#[wasm_bindgen] +pub fn hash(input: &[u8]) -> Uint8Array { + pubky_common::crypto::hash(input) + .as_bytes() + .as_slice() + .into() +} From 0fe5e1aa74d15a755f8230f5128c0d0c735dbbcd Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 1 Sep 2024 19:33:04 +0300 Subject: [PATCH 106/125] feat(common): simplify Capabilities --- pubky-common/src/auth.rs | 8 +- pubky-common/src/capabilities.rs | 160 ++++++++++++++----------------- pubky-common/src/session.rs | 6 +- pubky/src/shared/auth.rs | 8 +- 4 files changed, 86 insertions(+), 96 deletions(-) diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 71e69a5..68fafb4 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -207,7 +207,7 @@ mod tests { fn v0_id_signable() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let capabilities = vec![Capability::pubky_root()]; + let capabilities = vec![Capability::root()]; let token = AuthToken::sign(&signer, &audience, capabilities.clone()); @@ -228,7 +228,7 @@ mod tests { fn sign_verify() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let capabilities = vec![Capability::pubky_root()]; + let capabilities = vec![Capability::root()]; let verifier = AuthVerifier::new(audience.clone()); @@ -245,7 +245,7 @@ mod tests { fn expired() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let capabilities = vec![Capability::pubky_root()]; + let capabilities = vec![Capability::root()]; let verifier = AuthVerifier::new(audience.clone()); @@ -278,7 +278,7 @@ mod tests { fn already_used() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let capabilities = vec![Capability::pubky_root()]; + let capabilities = vec![Capability::root()]; let verifier = AuthVerifier::new(audience.clone()); diff --git a/pubky-common/src/capabilities.rs b/pubky-common/src/capabilities.rs index 2a46cde..ece2a3f 100644 --- a/pubky-common/src/capabilities.rs +++ b/pubky-common/src/capabilities.rs @@ -2,56 +2,50 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -const PUBKY_CAP_PREFIX: &str = "pk!"; - #[derive(Debug, Clone, PartialEq, Eq)] -pub enum Capability { - /// Pubky Homeserver's capabilities - Pubky(PubkyCap), - Unknown(String), +pub struct Capability { + pub resource: String, + pub abilities: Vec, } impl Capability { - /// Create a [PubkyCap] at the root path `/` with all the available [PubkyAbility] - pub fn pubky_root() -> Self { - Capability::Pubky(PubkyCap { - path: "/".to_string(), - abilities: vec![PubkyAbility::Read, PubkyAbility::Write], - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PubkyCap { - pub path: String, - pub abilities: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PubkyAbility { - /// Can read the resource at the specified path (GET requests). - Read, - /// Can write to the resource at the specified path (PUT/POST/DELETE requests). - Write, -} - -impl From<&PubkyAbility> for char { - fn from(value: &PubkyAbility) -> Self { - match value { - PubkyAbility::Read => 'r', - PubkyAbility::Write => 'w', + /// Create a root [Capability] at the `/` path with all the available [PubkyAbility] + pub fn root() -> Self { + Capability { + resource: "/".to_string(), + abilities: vec![Ability::Read, Ability::Write], } } } -impl TryFrom for PubkyAbility { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Ability { + /// Can read the resource at the specified path (GET requests). + Read, + /// Can write to the resource at the specified path (PUT/POST/DELETE requests). + Write, + /// Unknown ability + Unknown(char), +} + +impl From<&Ability> for char { + fn from(value: &Ability) -> Self { + match value { + Ability::Read => 'r', + Ability::Write => 'w', + Ability::Unknown(char) => char.to_owned(), + } + } +} + +impl TryFrom for Ability { type Error = Error; fn try_from(value: char) -> Result { match value { 'r' => Ok(Self::Read), 'w' => Ok(Self::Write), - _ => Err(Error::InvalidPubkyAbility), + _ => Err(Error::InvalidAbility), } } } @@ -66,16 +60,12 @@ impl TryFrom for Capability { impl Display for Capability { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Pubky(cap) => write!( - f, - "{}{}:{}", - PUBKY_CAP_PREFIX, - cap.path, - cap.abilities.iter().map(char::from).collect::() - ), - Self::Unknown(string) => write!(f, "{string}"), - } + write!( + f, + "{}:{}", + self.resource, + self.abilities.iter().map(char::from).collect::() + ) } } @@ -83,48 +73,46 @@ impl TryFrom<&str> for Capability { type Error = Error; fn try_from(value: &str) -> Result { - if value.starts_with(PUBKY_CAP_PREFIX) { - let mut rsplit = value.rsplit(':'); - - let mut abilities = Vec::new(); - - for char in rsplit - .next() - .ok_or(Error::MissingField("abilities"))? - .chars() - { - let ability = PubkyAbility::try_from(char)?; - - match abilities.binary_search_by(|element| char::from(element).cmp(&char)) { - Ok(_) => {} - Err(index) => { - abilities.insert(index, ability); - } - } - } - - let path = rsplit.next().ok_or(Error::MissingField("path"))?[PUBKY_CAP_PREFIX.len()..] - .to_string(); - - if !path.starts_with('/') { - return Err(Error::InvalidPath); - } - - return Ok(Capability::Pubky(PubkyCap { path, abilities })); + if value.matches(':').count() != 1 { + return Err(Error::InvalidFormat); } - Ok(Capability::Unknown(value.to_string())) + if !value.starts_with('/') { + return Err(Error::InvalidResource); + } + + let abilities_str = value.rsplit(':').next().unwrap_or(""); + + let mut abilities = Vec::new(); + + for char in abilities_str.chars() { + let ability = Ability::try_from(char)?; + + match abilities.binary_search_by(|element| char::from(element).cmp(&char)) { + Ok(_) => {} + Err(index) => { + abilities.insert(index, ability); + } + } + } + + let resource = value[0..value.len() - abilities_str.len() - 1].to_string(); + + Ok(Capability { + resource, + abilities, + }) } } #[derive(thiserror::Error, Debug, PartialEq, Eq)] pub enum Error { - #[error("PubkyCap: Missing field {0}")] - MissingField(&'static str), - #[error("PubkyCap: InvalidPath does not start with `/`")] - InvalidPath, - #[error("Invalid PubkyAbility")] - InvalidPubkyAbility, + #[error("Capability: Invalid resource path: does not start with `/`")] + InvalidResource, + #[error("Capability: Invalid format should be :")] + InvalidFormat, + #[error("Capability: Invalid Ability")] + InvalidAbility, } impl Serialize for Capability { @@ -155,13 +143,13 @@ mod tests { #[test] fn pubky_caps() { - let cap = Capability::Pubky(PubkyCap { - path: "/pub/pubky.app/".to_string(), - abilities: vec![PubkyAbility::Read, PubkyAbility::Write], - }); + let cap = Capability { + resource: "/pub/pubky.app/".to_string(), + abilities: vec![Ability::Read, Ability::Write], + }; // Read and write withing directory `/pub/pubky.app/`. - let expected_string = "pk!/pub/pubky.app/:rw"; + let expected_string = "/pub/pubky.app/:rw"; assert_eq!(cap.to_string(), expected_string); diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 83be3a7..eb8ff2b 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -66,6 +66,8 @@ impl Session { Ok(from_bytes(bytes)?) } + + // TODO: add `can_read()`, `can_write()` and `is_root()` methods } pub type Result = core::result::Result; @@ -86,7 +88,7 @@ mod tests { fn serialize() { let session = Session { user_agent: "foo".to_string(), - capabilities: vec![Capability::pubky_root()], + capabilities: vec![Capability::root()], ..Default::default() }; @@ -94,7 +96,7 @@ mod tests { assert_eq!( serialized, - [0, 0, 0, 3, 102, 111, 111, 1, 7, 112, 107, 33, 47, 58, 114, 119] + [0, 0, 0, 3, 102, 111, 111, 1, 4, 47, 58, 114, 119] ); let deseiralized = Session::deserialize(&serialized).unwrap(); diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 6cbb65d..11b312e 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -26,7 +26,7 @@ impl PubkyClient { url.set_path("/signup"); - let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize(); + let body = AuthToken::sign(keypair, &audience, vec![Capability::root()]).serialize(); let response = self .request(Method::POST, url.clone()) @@ -89,7 +89,7 @@ impl PubkyClient { url.set_path("/session"); - let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize(); + let body = AuthToken::sign(keypair, &audience, vec![Capability::root()]).serialize(); let response = self.request(Method::POST, url).body(body).send().await?; @@ -125,7 +125,7 @@ mod tests { .unwrap() .unwrap(); - assert!(session.capabilities.contains(&Capability::pubky_root())); + assert!(session.capabilities.contains(&Capability::root())); client.signout(&keypair.public_key()).await.unwrap(); @@ -144,7 +144,7 @@ mod tests { .unwrap() .unwrap(); - assert!(session.capabilities.contains(&Capability::pubky_root())); + assert!(session.capabilities.contains(&Capability::root())); } } } From cf46ff849c42b2e7e92dcbccb4509569d9ca3d1a Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 2 Sep 2024 00:19:19 +0300 Subject: [PATCH 107/125] examples(authz): update svgs and add copied to clipboard note --- examples/authz/3rd-party-app/index.html | 2 +- examples/authz/3rd-party-app/public/pubky.svg | 1 + examples/authz/3rd-party-app/public/vite.svg | 1 - .../3rd-party-app/src/pubky-auth-widget.js | 53 ++++++++++++++++--- 4 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 examples/authz/3rd-party-app/public/pubky.svg delete mode 100644 examples/authz/3rd-party-app/public/vite.svg diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html index ef83865..3eafca8 100644 --- a/examples/authz/3rd-party-app/index.html +++ b/examples/authz/3rd-party-app/index.html @@ -2,7 +2,7 @@ - + Vite + Lit diff --git a/examples/authz/3rd-party-app/public/pubky.svg b/examples/authz/3rd-party-app/public/pubky.svg new file mode 100644 index 0000000..6802915 --- /dev/null +++ b/examples/authz/3rd-party-app/public/pubky.svg @@ -0,0 +1 @@ + diff --git a/examples/authz/3rd-party-app/public/vite.svg b/examples/authz/3rd-party-app/public/vite.svg deleted file mode 100644 index ee9fada..0000000 --- a/examples/authz/3rd-party-app/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js index 4535840..49ebe4c 100644 --- a/examples/authz/3rd-party-app/src/pubky-auth-widget.js +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -27,6 +27,10 @@ export class PubkyAuthWidget extends LitElement { * Widget's state (open or closed) */ open: { type: Boolean }, + /** + * Show "copied to clipboard" note + */ + showCopied: { type: Boolean }, } } @@ -58,10 +62,10 @@ export class PubkyAuthWidget extends LitElement { ) : DEFAULT_HTTP_RELAY - callbackUrl.pathname = callbackUrl.pathname + "/" + this.channelId - this.authUrl = `pubkyauth://${callbackUrl.hostname + callbackUrl.pathname}?capabilities=${this.caps}&secret=${base64url(this.secret)}`; - console.log({ url: this.authUrl }); + this.authUrl = `pubkyauth:///?relay=${callbackUrl.toString()}&capabilities=${this.caps}&secret=${base64url(this.secret)}`; + + callbackUrl.pathname = callbackUrl.pathname + "/" + this.channelId fetch(callbackUrl) .catch(error => console.error("PubkyAuthWidget: Failed to subscribe to http relay channel", error)) @@ -86,10 +90,11 @@ export class PubkyAuthWidget extends LitElement {
- + ` @@ -130,6 +135,16 @@ export class PubkyAuthWidget extends LitElement { } } + async _copyToClipboard() { + try { + await navigator.clipboard.writeText(this.authUrl); + this.showCopied = true; + setTimeout(() => { this.showCopied = false }, 1000) + } catch (error) { + console.error('Failed to copy text: ', error); + } + } + static get styles() { return css` * { @@ -148,6 +163,7 @@ export class PubkyAuthWidget extends LitElement { } button { + padding: 0; background: none; border: none; color: inherit; @@ -236,7 +252,7 @@ export class PubkyAuthWidget extends LitElement { line-height: 1rem; text-align: center; color: #fff; - opacity: .3; + opacity: .5; /* Fix flash wrap in open animation */ text-wrap: nowrap; @@ -248,6 +264,7 @@ export class PubkyAuthWidget extends LitElement { } .card { + position: relative; background: #3b3b3b; border-radius: 5px; padding: 1rem; @@ -268,7 +285,7 @@ export class PubkyAuthWidget extends LitElement { align-items: center; line-height: 1!important; - width: 90%; + width: 93%; overflow: hidden; text-overflow: ellipsis; text-wrap: nowrap; @@ -280,6 +297,28 @@ export class PubkyAuthWidget extends LitElement { flex: 1 1; margin-bottom: 1rem; } + + .copied { + will-change: opacity; + transition-property: opacity; + transition-duration: 80ms; + transition-timing-function: ease-in; + + opacity: 0; + + position: absolute; + right: 0; + top: -1.6rem; + font-size: 0.9em; + background: rgb(43 43 43 / 98%); + padding: .5rem; + border-radius: .3rem; + color: #ddd; + } + + .copied.show { + opacity:1 + } ` } } From 582e4b8259af2e10d67ae61b20ffa50b21999e8a Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 2 Sep 2024 00:20:55 +0300 Subject: [PATCH 108/125] refactor(common): rename AuthToken fields for clarity --- pubky-common/src/auth.rs | 74 ++++++++++++++--------------- pubky-homeserver/src/routes/auth.rs | 2 +- 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 68fafb4..5d85e3d 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -21,28 +21,29 @@ const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000; pub struct AuthToken { /// Version of the [AuthToken]. /// - /// Version 0: Signer is implicitly the same as the [AuthToken::subject] + /// Version 0: Signer is implicitly the same as the root keypair for + /// the [AuthToken::pubky], without any delegation. version: u8, /// Signature over the token. signature: Signature, /// Timestamp timestamp: Timestamp, /// The [PublicKey] of the owner of the resources being accessed by this token. - subject: PublicKey, + pubky: PublicKey, /// The Pubky of the party verifying the [AuthToken], for example a web server. - audience: PublicKey, + homeserver: PublicKey, // Variable length capabilities capabilities: Vec, } impl AuthToken { - pub fn sign(signer: &Keypair, audience: &PublicKey, capabilities: Vec) -> Self { + pub fn sign(keypair: &Keypair, homeserver: &PublicKey, capabilities: Vec) -> Self { let timestamp = Timestamp::now(); let mut token = Self { version: 0, - subject: signer.public_key(), - audience: audience.to_owned(), + pubky: keypair.public_key(), + homeserver: homeserver.to_owned(), timestamp, capabilities, signature: Signature::from_bytes(&[0; 64]), @@ -50,7 +51,7 @@ impl AuthToken { let serialized = token.serialize(); - token.signature = signer.sign(&serialized[65..]); + token.signature = keypair.sign(&serialized[65..]); token } @@ -59,12 +60,7 @@ impl AuthToken { &self.capabilities } - /// Authenticate signer to an audience directly with [] capailities. - /// - /// - // pub fn authn(signer: &Keypair, audience: &PublicKey) -> Self {} - - fn verify(audience: &PublicKey, bytes: &[u8]) -> Result { + fn verify(homeserver: &PublicKey, bytes: &[u8]) -> Result { if bytes[0] > CURRENT_VERSION { return Err(Error::UnknownVersion); } @@ -75,10 +71,10 @@ impl AuthToken { 0 => { let now = Timestamp::now(); - if &token.audience != audience { - return Err(Error::InvalidAudience( - audience.to_string(), - token.audience.to_string(), + if &token.homeserver != homeserver { + return Err(Error::InvalidHomeserver( + homeserver.to_string(), + token.homeserver.to_string(), )); } @@ -92,7 +88,7 @@ impl AuthToken { } token - .subject + .pubky .verify(AuthToken::signable(token.version, bytes), &token.signature) .map_err(|_| Error::InvalidSignature)?; @@ -106,8 +102,8 @@ impl AuthToken { postcard::to_allocvec(self).unwrap() } - pub fn subject(&self) -> &PublicKey { - &self.subject + pub fn pubky(&self) -> &PublicKey { + &self.pubky } /// A unique ID for this [AuthToken], which is a concatenation of @@ -131,14 +127,14 @@ impl AuthToken { #[derive(Debug, Clone)] pub struct AuthVerifier { - audience: PublicKey, + homeserver: PublicKey, seen: Arc>>>, } impl AuthVerifier { - pub fn new(audience: PublicKey) -> Self { + pub fn new(homeserver: PublicKey) -> Self { Self { - audience, + homeserver, seen: Arc::new(Mutex::new(Vec::new())), } } @@ -146,7 +142,7 @@ impl AuthVerifier { pub fn verify(&self, bytes: &[u8]) -> Result { self.gc(); - let token = AuthToken::verify(&self.audience, bytes)?; + let token = AuthToken::verify(&self.homeserver, bytes)?; let mut seen = self.seen.lock().unwrap(); @@ -181,8 +177,8 @@ impl AuthVerifier { pub enum Error { #[error("Unknown version")] UnknownVersion, - #[error("Invalid audience. Expected {0}, got {1}")] - InvalidAudience(String, String), + #[error("Invalid homeserver. Expected {0}, got {1}")] + InvalidHomeserver(String, String), #[error("AuthToken has a timestamp that is more than 45 seconds in the future")] TooFarInTheFuture, #[error("AuthToken has a timestamp that is more than 45 seconds in the past")] @@ -206,10 +202,10 @@ mod tests { #[test] fn v0_id_signable() { let signer = Keypair::random(); - let audience = Keypair::random().public_key(); + let homeserver = Keypair::random().public_key(); let capabilities = vec![Capability::root()]; - let token = AuthToken::sign(&signer, &audience, capabilities.clone()); + let token = AuthToken::sign(&signer, &homeserver, capabilities.clone()); let serialized = &token.serialize(); @@ -227,12 +223,12 @@ mod tests { #[test] fn sign_verify() { let signer = Keypair::random(); - let audience = Keypair::random().public_key(); + let homeserver = Keypair::random().public_key(); let capabilities = vec![Capability::root()]; - let verifier = AuthVerifier::new(audience.clone()); + let verifier = AuthVerifier::new(homeserver.clone()); - let token = AuthToken::sign(&signer, &audience, capabilities.clone()); + let token = AuthToken::sign(&signer, &homeserver, capabilities.clone()); let serialized = &token.serialize(); @@ -244,24 +240,24 @@ mod tests { #[test] fn expired() { let signer = Keypair::random(); - let audience = Keypair::random().public_key(); + let homeserver = Keypair::random().public_key(); let capabilities = vec![Capability::root()]; - let verifier = AuthVerifier::new(audience.clone()); + let verifier = AuthVerifier::new(homeserver.clone()); let timestamp = (&Timestamp::now()) - (TIMESTAMP_WINDOW as u64); let mut signable = vec![]; signable.extend_from_slice(signer.public_key().as_bytes()); - signable.extend_from_slice(audience.as_bytes()); + signable.extend_from_slice(homeserver.as_bytes()); signable.extend_from_slice(&postcard::to_allocvec(&capabilities).unwrap()); let signature = signer.sign(&signable); let token = AuthToken { version: 0, - subject: signer.public_key(), - audience, + pubky: signer.public_key(), + homeserver, timestamp, signature, capabilities, @@ -277,12 +273,12 @@ mod tests { #[test] fn already_used() { let signer = Keypair::random(); - let audience = Keypair::random().public_key(); + let homeserver = Keypair::random().public_key(); let capabilities = vec![Capability::root()]; - let verifier = AuthVerifier::new(audience.clone()); + let verifier = AuthVerifier::new(homeserver.clone()); - let token = AuthToken::sign(&signer, &audience, capabilities.clone()); + let token = AuthToken::sign(&signer, &homeserver, capabilities.clone()); let serialized = &token.serialize(); diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index a832e17..9a38cf3 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -94,7 +94,7 @@ pub async fn signin( ) -> Result { let token = state.verifier.verify(&body)?; - let public_key = token.subject(); + let public_key = token.pubky(); let mut wtxn = state.db.env.write_txn()?; From cd32df075b329e62523ffc278a46df2b639df41a Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 2 Sep 2024 00:21:27 +0300 Subject: [PATCH 109/125] feat(pubky): use rustls-tls feature in reqwest --- pubky/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 29c2c9b..ed3d6ba 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -17,13 +17,12 @@ url = "2.5.2" bytes = "^1.7.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } +pkarr = { workspace = true, features = ["async"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -pkarr = { workspace = true, features = ["async"] } -reqwest = { version = "0.12.5", features = ["cookies"], default-features = false } +reqwest = { version = "0.12.5", features = ["cookies", "rustls-tls"], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -pkarr = { workspace = true, default-features = false } reqwest = { version = "0.12.5", default-features = false } js-sys = "0.3.69" From 25f757f94dc296c1b54e62b0f6fa745656756ad7 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 2 Sep 2024 00:22:08 +0300 Subject: [PATCH 110/125] feat(pubky): add PubkyClient::authorize() --- Cargo.lock | 190 ++++++++++++++++++++--- examples/authz/authenticator/Cargo.toml | 1 + examples/authz/authenticator/src/main.rs | 65 ++++++-- pubky/src/native.rs | 7 + pubky/src/shared/auth.rs | 44 +++++- 5 files changed, 270 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29a7d4e..cf9bfbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,7 @@ dependencies = [ "pubky", "pubky-common", "rpassword", + "tokio", "url", ] @@ -1011,6 +1012,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.6" @@ -1216,13 +1235,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1250,16 +1270,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.36.1" @@ -1569,6 +1579,54 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quinn" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -1688,6 +1746,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "ipnet", "js-sys", @@ -1696,19 +1755,40 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rpassword" version = "7.3.1" @@ -1736,6 +1816,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1745,6 +1831,47 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -2107,34 +2234,44 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -2349,6 +2486,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.2" @@ -2469,6 +2612,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/examples/authz/authenticator/Cargo.toml b/examples/authz/authenticator/Cargo.toml index 2938b6d..6bce048 100644 --- a/examples/authz/authenticator/Cargo.toml +++ b/examples/authz/authenticator/Cargo.toml @@ -9,4 +9,5 @@ clap = { version = "4.5.16", features = ["derive"] } pubky = { version = "0.1.0", path = "../../../pubky" } pubky-common = { version = "0.1.0", path = "../../../pubky-common" } rpassword = "7.3.1" +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } url = "2.5.2" diff --git a/examples/authz/authenticator/src/main.rs b/examples/authz/authenticator/src/main.rs index 2643d7d..b5464e3 100644 --- a/examples/authz/authenticator/src/main.rs +++ b/examples/authz/authenticator/src/main.rs @@ -1,40 +1,79 @@ use anyhow::Result; use clap::Parser; +use pubky::PubkyClient; +use pubky_common::{auth::AuthToken, capabilities::Capability, crypto::PublicKey}; use std::path::PathBuf; use url::Url; +/// local testnet HOMESERVER +const HOMESERVER: &str = "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"; + #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Cli { + /// Path to a recovery_file of the Pubky you want to sign in with + recovery_file: PathBuf, + /// Pubky Auth url url: Url, - - /// Path to a recovery_file of the Pubky you want to sign in with - // #[arg(short, long, value_name = "FILE")] - recovery_file: PathBuf, - // /// Mutable data public key. - // public_key: String, } -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { let cli = Cli::parse(); let url = cli.url; - dbg!(url); + + let mut required_capabilities = vec![]; + let mut relay = "".to_string(); + let client_secret: [u8; 32]; + + for (name, value) in url.query_pairs() { + if name == "relay" { + relay = value.to_string(); + } + if name == "secret" { + // client_secret = value.to_string(); + } + if name == "capabilities" { + println!("\nRequired Capabilities:"); + + for cap_str in value.split(',') { + if let Ok(cap) = Capability::try_from(cap_str) { + println!(" {} : {:?}", cap.resource, cap.abilities); + required_capabilities.push(cap) + }; + } + } + } let recovery_file = std::fs::read(&cli.recovery_file)?; - println!("Successfully opened recovery file"); + // println!("\nSuccessfully opened recovery file"); - // // println!("Enter Pubky Auth URL to start the consent form:"); - // // let pubky_auth_url = rl.readline("> ")?; - // // dbg!(pubky_auth_url); + // === Consent form === - println!("Enter your recovery_file's passphrase to confirm:"); + println!("\nEnter your recovery_file's passphrase to confirm:"); let passphrase = rpassword::read_password()?; let keypair = pubky_common::recovery_file::decrypt_recovery_file(&recovery_file, &passphrase)?; println!("Successfully decrypted recovery file..."); + let client = PubkyClient::testnet(); + + // For the purposes of this demo, we need to make sure + // the user has an account on the local homeserver. + if client.signin(&keypair).await.is_err() { + client + .signup(&keypair, &PublicKey::try_from(HOMESERVER).unwrap()) + .await?; + }; + + client + .authorize(&keypair, required_capabilities, [0; 32], &relay) + .await?; + + println!("Sending AuthToken to the client..."); + Ok(()) } diff --git a/pubky/src/native.rs b/pubky/src/native.rs index ecae6b0..d782f08 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -69,6 +69,13 @@ impl PubkyClient { PubkyClientBuilder::default() } + pub fn testnet() -> Self { + Self::test(&Testnet { + bootstrap: vec!["localhost:6881".to_string()], + nodes: vec![], + }) + } + pub fn test(testnet: &Testnet) -> Self { let pkarr = PkarrClient::builder() .dht_settings(DhtSettings { diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 11b312e..abadc6c 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,7 +1,8 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; -use pubky_common::{auth::AuthToken, capabilities::Capability, session::Session}; +use pubky_common::{auth::AuthToken, capabilities::Capability, crypto::encrypt, session::Session}; +use url::Url; use crate::{error::Result, PubkyClient}; @@ -20,13 +21,13 @@ impl PubkyClient { let homeserver = homeserver.to_string(); let Endpoint { - public_key: audience, + public_key: server, mut url, } = self.resolve_endpoint(&homeserver).await?; url.set_path("/signup"); - let body = AuthToken::sign(keypair, &audience, vec![Capability::root()]).serialize(); + let body = AuthToken::sign(keypair, &server, vec![Capability::root()]).serialize(); let response = self .request(Method::POST, url.clone()) @@ -83,13 +84,13 @@ impl PubkyClient { let pubky = keypair.public_key(); let Endpoint { - public_key: audience, + public_key: homeserver, mut url, } = self.resolve_pubky_homeserver(&pubky).await?; url.set_path("/session"); - let body = AuthToken::sign(keypair, &audience, vec![Capability::root()]).serialize(); + let body = AuthToken::sign(keypair, &homeserver, vec![Capability::root()]).serialize(); let response = self.request(Method::POST, url).body(body).send().await?; @@ -97,6 +98,39 @@ impl PubkyClient { Ok(()) } + + pub async fn authorize( + &self, + keypair: &Keypair, + capabilities: Vec, + client_secret: [u8; 32], + relay: &str, + ) -> Result<()> { + let pubky = keypair.public_key(); + let Endpoint { + public_key: homeserver, + .. + } = self.resolve_pubky_homeserver(&pubky).await?; + + let token = AuthToken::sign(keypair, &homeserver, capabilities); + + let encrypted_token = encrypt(&token.serialize(), &client_secret)?; + + let mut callback = Url::parse(relay)?; + let mut path_segments = callback.path_segments_mut().unwrap(); + path_segments.push("8Y69yafXgEMafLJKoJ_Ht5zPOVMWuZx_HfKY03U4MTI"); + + drop(path_segments); + + dbg!(callback.to_string()); + + self.request(Method::POST, callback) + .body(encrypted_token) + .send() + .await?; + + Ok(()) + } } #[cfg(test)] From 002abbbbca15da3f3d6163a6619d3a7149d1007c Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 2 Sep 2024 09:40:01 +0300 Subject: [PATCH 111/125] feat(pubky): add PubkyClientBuilder::testnet() and skip setting dht_request_timeout in CI --- pubky/src/native.rs | 79 +++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 43 deletions(-) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 91adb02..7af0a05 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,9 +1,6 @@ use std::time::Duration; -use ::pkarr::{ - mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PublicKey, SignedPacket, -}; +use ::pkarr::{mainline::dht::Testnet, PkarrClient, PublicKey, SignedPacket}; use bytes::Bytes; use pkarr::Keypair; use pubky_common::session::Session; @@ -21,21 +18,30 @@ use crate::{ static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); -impl Default for PubkyClient { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug, Default)] pub struct PubkyClientBuilder { - pkarr_settings: Option, + pkarr_settings: pkarr::Settings, } impl PubkyClientBuilder { /// Set Pkarr client [pkarr::Settings]. pub fn pkarr_settings(mut self, settings: pkarr::Settings) -> Self { - self.pkarr_settings = settings.into(); + self.pkarr_settings = settings; + self + } + + /// Use the bootstrap nodes of a testnet, useful mostly in unit tests. + pub fn testnet(self, testnet: &Testnet) -> Self { + self.bootstrap(testnet.bootstrap.to_vec()) + } + + pub fn dht_request_timeout(mut self, timeout: Duration) -> Self { + self.pkarr_settings.dht.request_timeout = timeout.into(); + self + } + + pub fn bootstrap(mut self, bootstrap: Vec) -> Self { + self.pkarr_settings.dht.bootstrap = bootstrap.into(); self } @@ -47,51 +53,38 @@ impl PubkyClientBuilder { .user_agent(DEFAULT_USER_AGENT) .build() .unwrap(), - pkarr: PkarrClient::new(self.pkarr_settings.unwrap_or_default()) - .unwrap() - .as_async(), + pkarr: PkarrClient::new(self.pkarr_settings).unwrap().as_async(), } } } +impl Default for PubkyClient { + fn default() -> Self { + PubkyClient::builder().build() + } +} + // === Public API === impl PubkyClient { - pub fn new() -> Self { - Self { - http: reqwest::Client::builder() - .cookie_store(true) - .user_agent(DEFAULT_USER_AGENT) - .build() - .unwrap(), - pkarr: PkarrClient::new(Default::default()).unwrap().as_async(), - } - } - /// Returns a builder to edit settings before creating [PubkyClient]. pub fn builder() -> PubkyClientBuilder { PubkyClientBuilder::default() } - pub fn test(testnet: &Testnet) -> Self { - let pkarr = PkarrClient::builder() - .dht_settings(DhtSettings { - request_timeout: Some(Duration::from_millis(500)), - bootstrap: Some(testnet.bootstrap.to_owned()), - ..DhtSettings::default() - }) - .resolvers(testnet.bootstrap.clone().into()) - .build() - .unwrap() - .as_async(); + /// Creates a [PubkyClient] with: + /// - DHT bootstrap nodes set to the `testnet` bootstrap nodes. + /// - DHT request timout set to 500 milliseconds. (unless in CI, then it is left as default) + /// + /// For more control, you can use [PubkyClientBuilder::testnet] + pub fn test(testnet: &Testnet) -> PubkyClient { + let mut builder = PubkyClient::builder().testnet(testnet); - let http = reqwest::Client::builder() - .cookie_store(true) - .user_agent(DEFAULT_USER_AGENT) - .build() - .unwrap(); + if std::env::var("CI").is_err() { + builder = builder.dht_request_timeout(Duration::from_millis(100)); + } - Self { http, pkarr } + builder.build() } // === Auth === From 634d3097666c18aed1a7486a2708d545e22ade2c Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 2 Sep 2024 17:05:06 +0300 Subject: [PATCH 112/125] examples(authz): test thirdPartySignin() --- Cargo.lock | 2 + examples/authz/3rd-party-app/index.html | 2 +- .../3rd-party-app/src/pubky-auth-widget.js | 44 ++++++++++++- examples/authz/authenticator/Cargo.toml | 1 + examples/authz/authenticator/src/main.rs | 62 ++++++++++++------- pubky-common/src/auth.rs | 10 ++- pubky-homeserver/src/routes/auth.rs | 2 + pubky/Cargo.toml | 1 + pubky/pkg/test/auth.js | 4 ++ pubky/src/error.rs | 3 + pubky/src/shared/auth.rs | 51 +++++++++++++-- pubky/src/wasm.rs | 28 ++++++++- 12 files changed, 171 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf9bfbe..abf383f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,7 @@ name = "authenticator" version = "0.1.0" dependencies = [ "anyhow", + "base64 0.22.1", "clap", "pubky", "pubky-common", @@ -1511,6 +1512,7 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" name = "pubky" version = "0.1.0" dependencies = [ + "base64 0.22.1", "bytes", "js-sys", "pkarr", diff --git a/examples/authz/3rd-party-app/index.html b/examples/authz/3rd-party-app/index.html index 3eafca8..1249852 100644 --- a/examples/authz/3rd-party-app/index.html +++ b/examples/authz/3rd-party-app/index.html @@ -4,7 +4,7 @@ - Vite + Lit + Pubky Auth Demo