feat(pubky): add put/get methods for js

This commit is contained in:
nazeh
2024-07-29 13:13:56 +03:00
parent ce2a00f020
commit bd5b44e544
9 changed files with 220 additions and 44 deletions

View File

@@ -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" }

View File

@@ -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)

46
pubky/pkg/test/public.js Normal file
View File

@@ -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")

View File

@@ -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<Bytes> {
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)
}

View File

@@ -1,2 +1,3 @@
pub mod auth;
pub mod pkarr;
pub mod public;

View File

@@ -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<Bytes> {
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]))
}
}

View File

@@ -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<js_sys::Uint8Array, JsValue> {
self.inner_get(pubky.as_inner(), path)
.await
.map(|b| (*b).into())
.map_err(|e| e.into())
}
}

42
pubky/src/wasm/http.rs Normal file
View File

@@ -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));
}
}

View File

@@ -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<PublicKey, JsValue> {
let string = value.as_string().ok_or(Error::Generic(
"Couldn't create a PublicKey from this type of value".to_string(),