mirror of
https://github.com/aljazceru/pubky-core.git
synced 2026-01-10 09:44:20 +01:00
Merge pull request #36 from pubky/feat/pkarr-reqwest
Feat/pkarr reqwest
This commit is contained in:
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -1066,9 +1066,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.9.4"
|
||||
version = "1.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
|
||||
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
@@ -1689,6 +1689,7 @@ dependencies = [
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1899,9 +1900,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.7"
|
||||
version = "0.12.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
|
||||
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Strictly monotonic unix timestamp in microseconds
|
||||
//! Absolutely monotonic unix timestamp in microseconds
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
@@ -31,7 +31,7 @@ impl TimestampFactory {
|
||||
}
|
||||
|
||||
pub fn now(&mut self) -> Timestamp {
|
||||
// Ensure strict monotonicity.
|
||||
// Ensure absolute monotonicity.
|
||||
self.last_time = (system_time() & TIME_MASK).max(self.last_time + CLOCK_MASK + 1);
|
||||
|
||||
// Add clock_id to the end of the timestamp
|
||||
@@ -48,13 +48,13 @@ impl Default for TimestampFactory {
|
||||
static DEFAULT_FACTORY: Lazy<Mutex<TimestampFactory>> =
|
||||
Lazy::new(|| Mutex::new(TimestampFactory::default()));
|
||||
|
||||
/// STrictly monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64.
|
||||
/// Absolutely monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64.
|
||||
///
|
||||
/// The purpose of this timestamp is to unique per "user", not globally,
|
||||
/// it achieves this by:
|
||||
/// 1. Override the last byte with a random `clock_id`, reducing the probability
|
||||
/// of two matching timestamps across multiple machines/threads.
|
||||
/// 2. Gurantee that the remaining 3 bytes are ever increasing (strictly monotonic) within
|
||||
/// 2. Gurantee that the remaining 3 bytes are ever increasing (absolutely monotonic) within
|
||||
/// the same thread regardless of the wall clock value
|
||||
///
|
||||
/// This timestamp is also serialized as BE bytes to remain sortable.
|
||||
@@ -215,7 +215,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn strictly_monotonic() {
|
||||
fn absolutely_monotonic() {
|
||||
const COUNT: usize = 100;
|
||||
|
||||
let mut set = HashSet::with_capacity(COUNT);
|
||||
|
||||
@@ -168,8 +168,8 @@ impl Config {
|
||||
self.bootstrap.to_owned()
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> &Option<String> {
|
||||
&self.domain
|
||||
pub fn domain(&self) -> Option<&String> {
|
||||
self.domain.as_ref()
|
||||
}
|
||||
|
||||
pub fn keypair(&self) -> &Keypair {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Use testnet network (local DHT) for testing.
|
||||
testnet = false
|
||||
# testnet = false
|
||||
|
||||
# Secret key (in hex) to generate the Homeserver's Keypair
|
||||
secret_key = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
# Domain to be published in Pkarr records for this server to be accessible by.
|
||||
domain = "localhost"
|
||||
# secret_key = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
# ICANN domain pointing to this server to allow browsers to connect to it.
|
||||
# domain = "example.com"
|
||||
|
||||
# Port for the Homeserver to listen on.
|
||||
port = 6287
|
||||
|
||||
# Storage directory Defaults to <System's Data Directory>
|
||||
# storage = ""
|
||||
|
||||
@@ -1,44 +1,63 @@
|
||||
//! Pkarr related task
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use pkarr::{
|
||||
dns::{rdata::SVCB, Packet},
|
||||
Keypair, SignedPacket,
|
||||
dns::{
|
||||
rdata::{RData, A, SVCB},
|
||||
Packet,
|
||||
},
|
||||
SignedPacket,
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn publish_server_packet(
|
||||
pkarr_client: &pkarr::Client,
|
||||
keypair: &Keypair,
|
||||
domain: &str,
|
||||
config: &Config,
|
||||
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()?);
|
||||
let default = ".".to_string();
|
||||
let target = config.domain().unwrap_or(&default);
|
||||
let mut svcb = SVCB::new(0, target.as_str().try_into()?);
|
||||
|
||||
// Publishing port only for localhost domain,
|
||||
// assuming any other domain will point to a reverse proxy
|
||||
// at the conventional ports.
|
||||
if domain == "localhost" {
|
||||
svcb.priority = 1;
|
||||
svcb.set_port(port);
|
||||
|
||||
// TODO: Add more parameteres like the signer key!
|
||||
// svcb.set_param(key, value)
|
||||
};
|
||||
|
||||
// TODO: announce A/AAAA records as well for Noise connections?
|
||||
// Or maybe Iroh's magic socket
|
||||
svcb.priority = 1;
|
||||
svcb.set_port(port);
|
||||
|
||||
packet.answers.push(pkarr::dns::ResourceRecord::new(
|
||||
"@".try_into().unwrap(),
|
||||
pkarr::dns::CLASS::IN,
|
||||
60 * 60,
|
||||
pkarr::dns::rdata::RData::SVCB(svcb),
|
||||
RData::HTTPS(svcb.clone().into()),
|
||||
));
|
||||
|
||||
let signed_packet = SignedPacket::from_packet(keypair, &packet)?;
|
||||
if config.domain().is_none() {
|
||||
// TODO: remove after remvoing Pubky shared/public
|
||||
// and add local host IP address instead.
|
||||
svcb.target = "localhost".try_into().unwrap();
|
||||
|
||||
packet.answers.push(pkarr::dns::ResourceRecord::new(
|
||||
"@".try_into().unwrap(),
|
||||
pkarr::dns::CLASS::IN,
|
||||
60 * 60,
|
||||
RData::HTTPS(svcb.clone().into()),
|
||||
));
|
||||
|
||||
packet.answers.push(pkarr::dns::ResourceRecord::new(
|
||||
"@".try_into().unwrap(),
|
||||
pkarr::dns::CLASS::IN,
|
||||
60 * 60,
|
||||
RData::A(A::from(Ipv4Addr::from([127, 0, 0, 1]))),
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: announce A/AAAA records as well for TLS connections?
|
||||
|
||||
let signed_packet = SignedPacket::from_packet(config.keypair(), &packet)?;
|
||||
|
||||
pkarr_client.publish(&signed_packet).await?;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Homeserver {
|
||||
let state = AppState {
|
||||
verifier: AuthVerifier::default(),
|
||||
db,
|
||||
pkarr_client,
|
||||
pkarr_client: pkarr_client.clone(),
|
||||
config: config.clone(),
|
||||
port,
|
||||
};
|
||||
@@ -70,20 +70,10 @@ impl Homeserver {
|
||||
|
||||
info!("Homeserver listening on http://localhost:{port}");
|
||||
|
||||
publish_server_packet(
|
||||
&state.pkarr_client,
|
||||
config.keypair(),
|
||||
&state
|
||||
.config
|
||||
.domain()
|
||||
.clone()
|
||||
.unwrap_or("localhost".to_string()),
|
||||
port,
|
||||
)
|
||||
.await?;
|
||||
publish_server_packet(&pkarr_client, &config, port).await?;
|
||||
|
||||
info!(
|
||||
"Homeserver listening on pubky://{}",
|
||||
"Homeserver listening on http://{}",
|
||||
config.keypair().public_key()
|
||||
);
|
||||
|
||||
|
||||
@@ -18,19 +18,23 @@ bytes = "^1.7.1"
|
||||
base64 = "0.22.1"
|
||||
|
||||
pubky-common = { version = "0.1.0", path = "../pubky-common" }
|
||||
pkarr = { workspace = true }
|
||||
pkarr = { workspace = true, features = ["endpoints"] }
|
||||
|
||||
# Native dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
reqwest = { version = "0.12.5", features = ["cookies", "rustls-tls"], default-features = false }
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
|
||||
# Wasm dependencies
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
reqwest = { version = "0.12.5", default-features = false }
|
||||
|
||||
js-sys = "0.3.69"
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen-futures = "0.4.42"
|
||||
|
||||
js-sys = "0.3.69"
|
||||
web-sys = "0.3.70"
|
||||
|
||||
[dev-dependencies]
|
||||
pubky_homeserver = { path = "../pubky-homeserver" }
|
||||
tokio = "1.37.0"
|
||||
|
||||
1
pubky/pkg/.gitignore
vendored
1
pubky/pkg/.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
index.cjs
|
||||
browser.js
|
||||
coverage
|
||||
node_modules
|
||||
|
||||
6
pubky/pkg/index.cjs
Normal file
6
pubky/pkg/index.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
const makeFetchCookie = require("fetch-cookie").default;
|
||||
|
||||
let originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = makeFetchCookie(originalFetch);
|
||||
|
||||
module.exports = require('./nodejs/pubky')
|
||||
@@ -37,5 +37,8 @@
|
||||
"esmify": "^2.1.1",
|
||||
"tape": "^5.8.1",
|
||||
"tape-run": "^11.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"fetch-cookie": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
26
pubky/pkg/test/http.js
Normal file
26
pubky/pkg/test/http.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import test from 'tape'
|
||||
|
||||
import { PubkyClient, Keypair, PublicKey } from '../index.cjs'
|
||||
|
||||
const TLD = '8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo';
|
||||
|
||||
// TODO: test HTTPs too somehow.
|
||||
|
||||
test("basic fetch", async (t) => {
|
||||
let client = PubkyClient.testnet();
|
||||
|
||||
// Normal TLD
|
||||
{
|
||||
|
||||
let response = await client.fetch(`http://relay.pkarr.org/`);
|
||||
|
||||
t.equal(response.status, 200);
|
||||
}
|
||||
|
||||
|
||||
// Pubky
|
||||
let response = await client.fetch(`http://${TLD}/`);
|
||||
|
||||
t.equal(response.status, 200);
|
||||
})
|
||||
|
||||
@@ -55,12 +55,3 @@ const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `.
|
||||
);
|
||||
|
||||
await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched + "\nglobalThis['pubky'] = imports");
|
||||
|
||||
// Move outside of nodejs
|
||||
|
||||
await Promise.all([".js", ".d.ts", "_bg.wasm"].map(suffix =>
|
||||
rename(
|
||||
path.join(__dirname, `../../pkg/nodejs/${name}${suffix}`),
|
||||
path.join(__dirname, `../../pkg/${suffix === '.js' ? "index.cjs" : (name + suffix)}`),
|
||||
))
|
||||
)
|
||||
|
||||
@@ -12,9 +12,6 @@ pub enum Error {
|
||||
#[error("Generic error: {0}")]
|
||||
Generic(String),
|
||||
|
||||
#[error("Could not resolve endpoint for {0}")]
|
||||
ResolveEndpoint(String),
|
||||
|
||||
#[error("Could not convert the passed type into a Url")]
|
||||
InvalidUrl,
|
||||
|
||||
@@ -42,6 +39,9 @@ pub enum Error {
|
||||
|
||||
#[error(transparent)]
|
||||
AuthToken(#[from] pubky_common::auth::Error),
|
||||
|
||||
#[error("Could not resolve Endpoint for domain: {0}")]
|
||||
ResolveEndpoint(String),
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -6,11 +6,6 @@ mod native;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -23,11 +18,5 @@ pub use crate::shared::list_builder::ListBuilder;
|
||||
#[wasm_bindgen]
|
||||
pub struct PubkyClient {
|
||||
http: reqwest::Client,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) pkarr: pkarr::Client,
|
||||
/// A cookie jar for nodejs fetch.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) session_cookies: Arc<RwLock<HashSet<String>>>,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) pkarr_relays: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::time::Duration;
|
||||
use std::{net::ToSocketAddrs, sync::Arc};
|
||||
|
||||
use bytes::Bytes;
|
||||
use pubky_common::{
|
||||
capabilities::Capabilities,
|
||||
recovery_file::{create_recovery_file, decrypt_recovery_file},
|
||||
session::Session,
|
||||
};
|
||||
use reqwest::{RequestBuilder, Response};
|
||||
use tokio::sync::oneshot;
|
||||
use url::Url;
|
||||
use ::pkarr::mainline::dht::Testnet;
|
||||
|
||||
use pkarr::Keypair;
|
||||
use crate::PubkyClient;
|
||||
|
||||
use ::pkarr::{mainline::dht::Testnet, PublicKey, SignedPacket};
|
||||
mod api;
|
||||
mod internals;
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
shared::list_builder::ListBuilder,
|
||||
PubkyClient,
|
||||
};
|
||||
use internals::PkarrResolver;
|
||||
|
||||
static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
@@ -63,13 +52,19 @@ impl PubkyClientBuilder {
|
||||
|
||||
/// Build [PubkyClient]
|
||||
pub fn build(self) -> PubkyClient {
|
||||
// TODO: convert to Result<PubkyClient>
|
||||
|
||||
let pkarr = pkarr::Client::new(self.pkarr_settings).unwrap();
|
||||
let dns_resolver: PkarrResolver = (&pkarr).into();
|
||||
|
||||
PubkyClient {
|
||||
http: reqwest::Client::builder()
|
||||
.cookie_store(true)
|
||||
.dns_resolver(Arc::new(dns_resolver))
|
||||
.user_agent(DEFAULT_USER_AGENT)
|
||||
.build()
|
||||
.unwrap(),
|
||||
pkarr: pkarr::Client::new(self.pkarr_settings).unwrap(),
|
||||
pkarr,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,8 +75,6 @@ impl Default for PubkyClient {
|
||||
}
|
||||
}
|
||||
|
||||
// === Public API ===
|
||||
|
||||
impl PubkyClient {
|
||||
/// Returns a builder to edit settings before creating [PubkyClient].
|
||||
pub fn builder() -> PubkyClientBuilder {
|
||||
@@ -111,148 +104,4 @@ impl PubkyClient {
|
||||
|
||||
builder.build()
|
||||
}
|
||||
|
||||
// === Getters ===
|
||||
|
||||
/// Returns a reference to the internal [pkarr] Client.
|
||||
pub fn pkarr(&self) -> &pkarr::Client {
|
||||
&self.pkarr
|
||||
}
|
||||
|
||||
// === Auth ===
|
||||
|
||||
/// Signup to a homeserver and update Pkarr accordingly.
|
||||
///
|
||||
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
|
||||
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
|
||||
pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<Session> {
|
||||
self.inner_signup(keypair, homeserver).await
|
||||
}
|
||||
|
||||
/// Check the current sesison for a given Pubky in its homeserver.
|
||||
///
|
||||
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
|
||||
/// or [reqwest::Error] if the response has any other `>=400` status code.
|
||||
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>> {
|
||||
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<Session> {
|
||||
self.inner_signin(keypair).await
|
||||
}
|
||||
|
||||
// === Public data ===
|
||||
|
||||
/// Upload a small payload to a given path.
|
||||
pub async fn put<T: TryInto<Url>>(&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<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
|
||||
self.inner_get(url).await
|
||||
}
|
||||
|
||||
/// Delete a file at a path relative to a pubky author.
|
||||
pub async fn delete<T: TryInto<Url>>(&self, url: T) -> Result<()> {
|
||||
self.inner_delete(url).await
|
||||
}
|
||||
|
||||
/// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send].
|
||||
///
|
||||
/// `url` sets the path you want to lest within.
|
||||
pub fn list<T: TryInto<Url>>(&self, url: T) -> Result<ListBuilder> {
|
||||
self.inner_list(url)
|
||||
}
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
/// Create a recovery file of the `keypair`, containing the secret key encrypted
|
||||
/// using the `passphrase`.
|
||||
pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result<Vec<u8>> {
|
||||
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<Keypair> {
|
||||
Ok(decrypt_recovery_file(recovery_file, passphrase)?)
|
||||
}
|
||||
|
||||
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
|
||||
/// verifying that AuthToken, and if capabilities were requested, signing in to
|
||||
/// the Pubky's homeserver and returning the [Session] information.
|
||||
pub fn auth_request(
|
||||
&self,
|
||||
relay: impl TryInto<Url>,
|
||||
capabilities: &Capabilities,
|
||||
) -> Result<(Url, tokio::sync::oneshot::Receiver<PublicKey>)> {
|
||||
let mut relay: Url = relay
|
||||
.try_into()
|
||||
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
|
||||
|
||||
let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?;
|
||||
|
||||
let (tx, rx) = oneshot::channel::<PublicKey>();
|
||||
|
||||
let this = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let to_send = this
|
||||
.subscribe_to_auth_response(relay, &client_secret)
|
||||
.await?;
|
||||
|
||||
tx.send(to_send)
|
||||
.map_err(|_| Error::Generic("Failed to send the session after signing in with token, since the receiver is dropped".into()))?;
|
||||
|
||||
Ok::<(), Error>(())
|
||||
});
|
||||
|
||||
Ok((pubkyauth_url, rx))
|
||||
}
|
||||
|
||||
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
|
||||
/// source of the pubkyauth request url.
|
||||
pub async fn send_auth_token<T: TryInto<Url>>(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
pubkyauth_url: T,
|
||||
) -> Result<()> {
|
||||
let url: Url = pubkyauth_url.try_into().map_err(|_| Error::InvalidUrl)?;
|
||||
|
||||
self.inner_send_auth_token(keypair, url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// === Internals ===
|
||||
|
||||
impl PubkyClient {
|
||||
// === Pkarr ===
|
||||
|
||||
pub(crate) async fn pkarr_resolve(
|
||||
&self,
|
||||
public_key: &PublicKey,
|
||||
) -> Result<Option<SignedPacket>> {
|
||||
Ok(self.pkarr.resolve(public_key).await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> {
|
||||
Ok(self.pkarr.publish(signed_packet).await?)
|
||||
}
|
||||
|
||||
// === HTTP ===
|
||||
|
||||
pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder {
|
||||
self.http.request(method, url)
|
||||
}
|
||||
|
||||
pub(crate) fn store_session(&self, _: &Response) {}
|
||||
pub(crate) fn remove_session(&self, _: &PublicKey) {}
|
||||
}
|
||||
|
||||
85
pubky/src/native/api/auth.rs
Normal file
85
pubky/src/native/api/auth.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use pkarr::Keypair;
|
||||
use pubky_common::session::Session;
|
||||
use tokio::sync::oneshot;
|
||||
use url::Url;
|
||||
|
||||
use pkarr::PublicKey;
|
||||
|
||||
use pubky_common::capabilities::Capabilities;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::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: &PublicKey) -> Result<Session> {
|
||||
self.inner_signup(keypair, homeserver).await
|
||||
}
|
||||
|
||||
/// Check the current sesison for a given Pubky in its homeserver.
|
||||
///
|
||||
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
|
||||
/// or [reqwest::Error] if the response has any other `>=400` status code.
|
||||
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>> {
|
||||
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<Session> {
|
||||
self.inner_signin(keypair).await
|
||||
}
|
||||
|
||||
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
|
||||
/// verifying that AuthToken, and if capabilities were requested, signing in to
|
||||
/// the Pubky's homeserver and returning the [Session] information.
|
||||
pub fn auth_request(
|
||||
&self,
|
||||
relay: impl TryInto<Url>,
|
||||
capabilities: &Capabilities,
|
||||
) -> Result<(Url, tokio::sync::oneshot::Receiver<PublicKey>)> {
|
||||
let mut relay: Url = relay
|
||||
.try_into()
|
||||
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
|
||||
|
||||
let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?;
|
||||
|
||||
let (tx, rx) = oneshot::channel::<PublicKey>();
|
||||
|
||||
let this = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let to_send = this
|
||||
.subscribe_to_auth_response(relay, &client_secret)
|
||||
.await?;
|
||||
|
||||
tx.send(to_send)
|
||||
.map_err(|_| Error::Generic("Failed to send the session after signing in with token, since the receiver is dropped".into()))?;
|
||||
|
||||
Ok::<(), Error>(())
|
||||
});
|
||||
|
||||
Ok((pubkyauth_url, rx))
|
||||
}
|
||||
|
||||
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
|
||||
/// source of the pubkyauth request url.
|
||||
pub async fn send_auth_token<T: TryInto<Url>>(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
pubkyauth_url: T,
|
||||
) -> Result<()> {
|
||||
let url: Url = pubkyauth_url.try_into().map_err(|_| Error::InvalidUrl)?;
|
||||
|
||||
self.inner_send_auth_token(keypair, url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
65
pubky/src/native/api/http.rs
Normal file
65
pubky/src/native/api/http.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use reqwest::{IntoUrl, Method, RequestBuilder};
|
||||
|
||||
use crate::PubkyClient;
|
||||
|
||||
impl PubkyClient {
|
||||
/// Start building a `Request` with the `Method` and `Url`.
|
||||
///
|
||||
/// Returns a `RequestBuilder`, which will allow setting headers and
|
||||
/// the request body before sending.
|
||||
///
|
||||
/// Differs from [reqwest::Client::request], in that it can make requests
|
||||
/// to URLs with a [pkarr::PublicKey] as Top Level Domain, by resolving
|
||||
/// corresponding endpoints, and verifying TLS certificates accordingly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This method fails whenever the supplied `Url` cannot be parsed.
|
||||
pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
|
||||
self.http.request(method, url)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pkarr::mainline::Testnet;
|
||||
use pubky_homeserver::Homeserver;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get_pubky() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
|
||||
let homeserver = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = PubkyClient::builder().testnet(&testnet).build();
|
||||
|
||||
let url = format!("http://{}/", homeserver.public_key());
|
||||
|
||||
let response = client
|
||||
.request(Default::default(), url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get_icann() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
|
||||
let client = PubkyClient::builder().testnet(&testnet).build();
|
||||
|
||||
let url = format!("http://example.com/");
|
||||
|
||||
let response = client
|
||||
.request(Default::default(), url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
}
|
||||
6
pubky/src/native/api/mod.rs
Normal file
6
pubky/src/native/api/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod http;
|
||||
pub mod recovery_file;
|
||||
|
||||
// TODO: put the Homeserver API behind a feature flag
|
||||
pub mod auth;
|
||||
pub mod public;
|
||||
28
pubky/src/native/api/public.rs
Normal file
28
pubky/src/native/api/public.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use bytes::Bytes;
|
||||
use url::Url;
|
||||
|
||||
use crate::{error::Result, shared::list_builder::ListBuilder, PubkyClient};
|
||||
|
||||
impl PubkyClient {
|
||||
/// Upload a small payload to a given path.
|
||||
pub async fn put<T: TryInto<Url>>(&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<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
|
||||
self.inner_get(url).await
|
||||
}
|
||||
|
||||
/// Delete a file at a path relative to a pubky author.
|
||||
pub async fn delete<T: TryInto<Url>>(&self, url: T) -> Result<()> {
|
||||
self.inner_delete(url).await
|
||||
}
|
||||
|
||||
/// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send].
|
||||
///
|
||||
/// `url` sets the path you want to lest within.
|
||||
pub fn list<T: TryInto<Url>>(&self, url: T) -> Result<ListBuilder> {
|
||||
self.inner_list(url)
|
||||
}
|
||||
}
|
||||
19
pubky/src/native/api/recovery_file.rs
Normal file
19
pubky/src/native/api/recovery_file.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use pubky_common::{
|
||||
crypto::Keypair,
|
||||
recovery_file::{create_recovery_file, decrypt_recovery_file},
|
||||
};
|
||||
|
||||
use crate::{error::Result, PubkyClient};
|
||||
|
||||
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<Vec<u8>> {
|
||||
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<Keypair> {
|
||||
Ok(decrypt_recovery_file(recovery_file, passphrase)?)
|
||||
}
|
||||
}
|
||||
45
pubky/src/native/internals.rs
Normal file
45
pubky/src/native/internals.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use reqwest::RequestBuilder;
|
||||
use url::Url;
|
||||
|
||||
use crate::PubkyClient;
|
||||
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use pkarr::{Client, EndpointResolver, PublicKey};
|
||||
use reqwest::dns::{Addrs, Resolve};
|
||||
|
||||
pub struct PkarrResolver(Client);
|
||||
|
||||
impl Resolve for PkarrResolver {
|
||||
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
|
||||
let client = self.0.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let name = name.as_str();
|
||||
|
||||
if PublicKey::try_from(name).is_ok() {
|
||||
let endpoint = client.resolve_endpoint(name).await?;
|
||||
|
||||
let addrs: Addrs = Box::new(endpoint.to_socket_addrs().into_iter());
|
||||
return Ok(addrs);
|
||||
};
|
||||
|
||||
Ok(Box::new(format!("{name}:0").to_socket_addrs().unwrap()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&pkarr::Client> for PkarrResolver {
|
||||
fn from(pkarr: &pkarr::Client) -> Self {
|
||||
PkarrResolver(pkarr.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl PubkyClient {
|
||||
// === HTTP ===
|
||||
|
||||
/// A wrapper around [reqwest::Client::request], with the same signature between native and wasm.
|
||||
pub(crate) async fn inner_request(&self, method: reqwest::Method, url: Url) -> RequestBuilder {
|
||||
self.http.request(method, url)
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,12 @@ impl PubkyClient {
|
||||
let body = AuthToken::sign(keypair, vec![Capability::root()]).serialize();
|
||||
|
||||
let response = self
|
||||
.request(Method::POST, url.clone())
|
||||
.inner_request(Method::POST, url.clone())
|
||||
.await
|
||||
.body(body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
self.store_session(&response);
|
||||
|
||||
self.publish_pubky_homeserver(keypair, &homeserver).await?;
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
@@ -61,7 +60,7 @@ impl PubkyClient {
|
||||
|
||||
url.set_path(&format!("/{}/session", pubky));
|
||||
|
||||
let res = self.request(Method::GET, url).send().await?;
|
||||
let res = self.inner_request(Method::GET, url).await.send().await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
@@ -82,9 +81,7 @@ impl PubkyClient {
|
||||
|
||||
url.set_path(&format!("/{}/session", pubky));
|
||||
|
||||
self.request(Method::DELETE, url).send().await?;
|
||||
|
||||
self.remove_session(pubky);
|
||||
self.inner_request(Method::DELETE, url).await.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -143,7 +140,8 @@ impl PubkyClient {
|
||||
path_segments.push(&channel_id);
|
||||
drop(path_segments);
|
||||
|
||||
self.request(Method::POST, callback)
|
||||
self.inner_request(Method::POST, callback)
|
||||
.await
|
||||
.body(encrypted_token)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -170,13 +168,12 @@ impl PubkyClient {
|
||||
self.resolve_url(&mut url).await?;
|
||||
|
||||
let response = self
|
||||
.request(Method::POST, url)
|
||||
.inner_request(Method::POST, url)
|
||||
.await
|
||||
.body(token.serialize())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
self.store_session(&response);
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(Session::deserialize(&bytes)?)
|
||||
@@ -309,6 +306,9 @@ mod tests {
|
||||
|
||||
assert_eq!(&public_key, &pubky);
|
||||
|
||||
let session = client.session(&pubky).await.unwrap().unwrap();
|
||||
assert_eq!(session.capabilities(), &capabilities.0);
|
||||
|
||||
// Test access control enforcement
|
||||
|
||||
client
|
||||
|
||||
@@ -90,7 +90,12 @@ impl<'a> ListBuilder<'a> {
|
||||
|
||||
drop(query);
|
||||
|
||||
let response = self.client.request(Method::GET, url).send().await?;
|
||||
let response = self
|
||||
.client
|
||||
.inner_request(Method::GET, url)
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
response.error_for_status_ref()?;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ impl PubkyClient {
|
||||
keypair: &Keypair,
|
||||
host: &str,
|
||||
) -> Result<()> {
|
||||
let existing = self.pkarr_resolve(&keypair.public_key()).await?;
|
||||
let existing = self.pkarr.resolve(&keypair.public_key()).await?;
|
||||
|
||||
let mut packet = Packet::new_reply(0);
|
||||
|
||||
@@ -42,7 +42,7 @@ impl PubkyClient {
|
||||
|
||||
let signed_packet = SignedPacket::from_packet(keypair, &packet)?;
|
||||
|
||||
self.pkarr_publish(&signed_packet).await?;
|
||||
self.pkarr.publish(&signed_packet).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -81,7 +81,8 @@ impl PubkyClient {
|
||||
step += 1;
|
||||
|
||||
if let Some(signed_packet) = self
|
||||
.pkarr_resolve(&public_key)
|
||||
.pkarr
|
||||
.resolve(&public_key)
|
||||
.await
|
||||
.map_err(|_| Error::ResolveEndpoint(original_target.into()))?
|
||||
{
|
||||
@@ -185,8 +186,8 @@ mod tests {
|
||||
rdata::{HTTPS, SVCB},
|
||||
Packet,
|
||||
},
|
||||
mainline::{dht::DhtSettings, Testnet},
|
||||
Keypair, Settings, SignedPacket,
|
||||
mainline::Testnet,
|
||||
Keypair, SignedPacket,
|
||||
};
|
||||
use pubky_homeserver::Homeserver;
|
||||
|
||||
@@ -194,14 +195,7 @@ mod tests {
|
||||
async fn resolve_endpoint_https() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
|
||||
let pkarr_client = pkarr::Client::new(Settings {
|
||||
dht: DhtSettings {
|
||||
bootstrap: Some(testnet.bootstrap.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
let pkarr_client = pkarr::Client::builder().testnet(&testnet).build().unwrap();
|
||||
|
||||
let domain = "example.com";
|
||||
let mut target;
|
||||
@@ -284,14 +278,7 @@ mod tests {
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
// Publish an intermediate controller of the homeserver
|
||||
let pkarr_client = pkarr::Client::new(Settings {
|
||||
dht: DhtSettings {
|
||||
bootstrap: Some(testnet.bootstrap.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
let pkarr_client = pkarr::Client::builder().testnet(&testnet).build().unwrap();
|
||||
|
||||
let intermediate = Keypair::random();
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ impl PubkyClient {
|
||||
let url = self.pubky_to_http(url).await?;
|
||||
|
||||
let response = self
|
||||
.request(Method::PUT, url)
|
||||
.inner_request(Method::PUT, url)
|
||||
.await
|
||||
.body(content.to_owned())
|
||||
.send()
|
||||
.await?;
|
||||
@@ -29,7 +30,7 @@ impl PubkyClient {
|
||||
pub(crate) async fn inner_get<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
|
||||
let url = self.pubky_to_http(url).await?;
|
||||
|
||||
let response = self.request(Method::GET, url).send().await?;
|
||||
let response = self.inner_request(Method::GET, url).await.send().await?;
|
||||
|
||||
if response.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
@@ -46,7 +47,7 @@ impl PubkyClient {
|
||||
pub(crate) async fn inner_delete<T: TryInto<Url>>(&self, url: T) -> Result<()> {
|
||||
let url = self.pubky_to_http(url).await?;
|
||||
|
||||
let response = self.request(Method::DELETE, url).send().await?;
|
||||
let response = self.inner_request(Method::DELETE, url).await.send().await?;
|
||||
|
||||
response.error_for_status_ref()?;
|
||||
|
||||
@@ -650,10 +651,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(
|
||||
Method::GET,
|
||||
format!("{feed_url}?limit=10").as_str().try_into().unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -683,13 +681,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(
|
||||
Method::GET,
|
||||
format!("{feed_url}?limit=10&cursor={cursor}")
|
||||
.as_str()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -740,10 +732,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(
|
||||
Method::GET,
|
||||
format!("{feed_url}?limit=10").as_str().try_into().unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -762,8 +751,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let get = client.get(url.as_str()).await.unwrap();
|
||||
dbg!(get);
|
||||
let get = client.get(url.as_str()).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(get.as_ref(), &[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -800,10 +790,7 @@ mod tests {
|
||||
let feed_url = format!("http://localhost:{}/events/", homeserver.port());
|
||||
|
||||
let response = client
|
||||
.request(
|
||||
Method::GET,
|
||||
format!("{feed_url}").as_str().try_into().unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use js_sys::{Array, Uint8Array};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use pubky_common::capabilities::Capabilities;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::PubkyClient;
|
||||
|
||||
mod http;
|
||||
mod keys;
|
||||
mod pkarr;
|
||||
mod recovery_file;
|
||||
mod session;
|
||||
|
||||
use keys::{Keypair, PublicKey};
|
||||
use session::Session;
|
||||
mod api;
|
||||
mod internals;
|
||||
mod wrappers;
|
||||
|
||||
impl Default for PubkyClient {
|
||||
fn default() -> Self {
|
||||
@@ -28,7 +12,6 @@ impl Default for PubkyClient {
|
||||
}
|
||||
}
|
||||
|
||||
static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"];
|
||||
static TESTNET_RELAYS: [&str; 1] = ["http://localhost:15411/pkarr"];
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -37,8 +20,7 @@ impl PubkyClient {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
http: reqwest::Client::builder().build().unwrap(),
|
||||
session_cookies: Arc::new(RwLock::new(HashSet::new())),
|
||||
pkarr_relays: DEFAULT_RELAYS.into_iter().map(|s| s.to_string()).collect(),
|
||||
pkarr: pkarr::Client::builder().build().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,203 +30,10 @@ impl PubkyClient {
|
||||
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(),
|
||||
pkarr: pkarr::Client::builder()
|
||||
.relays(TESTNET_RELAYS.into_iter().map(|s| s.to_string()).collect())
|
||||
.build()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<String>) -> 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<String> {
|
||||
self.pkarr_relays.clone()
|
||||
}
|
||||
|
||||
/// Signup to a homeserver and update Pkarr accordingly.
|
||||
///
|
||||
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
|
||||
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
|
||||
#[wasm_bindgen]
|
||||
pub async fn signup(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
homeserver: &PublicKey,
|
||||
) -> Result<Session, JsValue> {
|
||||
Ok(Session(
|
||||
self.inner_signup(keypair.as_inner(), homeserver.as_inner())
|
||||
.await
|
||||
.map_err(JsValue::from)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Check the current sesison for a given Pubky in its homeserver.
|
||||
///
|
||||
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
|
||||
/// or throws the recieved error if the response has any other `>=400` status code.
|
||||
#[wasm_bindgen]
|
||||
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>, JsValue> {
|
||||
self.inner_session(pubky.as_inner())
|
||||
.await
|
||||
.map(|s| s.map(Session))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Signout from a homeserver.
|
||||
#[wasm_bindgen]
|
||||
pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> {
|
||||
self.inner_signout(pubky.as_inner())
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Signin to a homeserver using the root Keypair.
|
||||
#[wasm_bindgen]
|
||||
pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> {
|
||||
self.inner_signin(keypair.as_inner())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
|
||||
/// verifying that AuthToken, and if capabilities were requested, signing in to
|
||||
/// the Pubky's homeserver and returning the [Session] information.
|
||||
///
|
||||
/// Returns a tuple of [pubkyAuthUrl, Promise<Session>]
|
||||
#[wasm_bindgen(js_name = "authRequest")]
|
||||
pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result<js_sys::Array, JsValue> {
|
||||
let mut relay: Url = relay
|
||||
.try_into()
|
||||
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
|
||||
|
||||
let (pubkyauth_url, client_secret) = self.create_auth_request(
|
||||
&mut relay,
|
||||
&Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?,
|
||||
)?;
|
||||
|
||||
let this = self.clone();
|
||||
|
||||
let future = async move {
|
||||
this.subscribe_to_auth_response(relay, &client_secret)
|
||||
.await
|
||||
.map(|pubky| JsValue::from(PublicKey(pubky)))
|
||||
.map_err(|err| JsValue::from_str(&format!("{:?}", err)))
|
||||
};
|
||||
|
||||
let promise = wasm_bindgen_futures::future_to_promise(future);
|
||||
|
||||
// Return the URL and the promise
|
||||
let js_tuple = js_sys::Array::new();
|
||||
js_tuple.push(&JsValue::from_str(pubkyauth_url.as_ref()));
|
||||
js_tuple.push(&promise);
|
||||
|
||||
Ok(js_tuple)
|
||||
}
|
||||
|
||||
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
|
||||
/// source of the pubkyauth request url.
|
||||
#[wasm_bindgen(js_name = "sendAuthToken")]
|
||||
pub async fn send_auth_token(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
pubkyauth_url: &str,
|
||||
) -> Result<(), JsValue> {
|
||||
let pubkyauth_url: Url = pubkyauth_url
|
||||
.try_into()
|
||||
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
|
||||
|
||||
self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// === Public data ===
|
||||
|
||||
#[wasm_bindgen]
|
||||
/// Upload a small payload to a given path.
|
||||
pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> {
|
||||
self.inner_put(url, content).await.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Download a small payload from a given path relative to a pubky author.
|
||||
#[wasm_bindgen]
|
||||
pub async fn get(&self, url: &str) -> Result<Option<Uint8Array>, JsValue> {
|
||||
self.inner_get(url)
|
||||
.await
|
||||
.map(|b| b.map(|b| (&*b).into()))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Delete a file at a path relative to a pubky author.
|
||||
#[wasm_bindgen]
|
||||
pub async fn delete(&self, url: &str) -> Result<(), JsValue> {
|
||||
self.inner_delete(url).await.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Returns a list of Pubky urls (as strings).
|
||||
///
|
||||
/// - `url`: The Pubky url (string) to the directory you want to list its content.
|
||||
/// - `cursor`: Either a full `pubky://` Url (from previous list response),
|
||||
/// or a path (to a file or directory) relative to the `url`
|
||||
/// - `reverse`: List in reverse order
|
||||
/// - `limit` Limit the number of urls in the response
|
||||
/// - `shallow`: List directories and files, instead of flat list of files.
|
||||
#[wasm_bindgen]
|
||||
pub async fn list(
|
||||
&self,
|
||||
url: &str,
|
||||
cursor: Option<String>,
|
||||
reverse: Option<bool>,
|
||||
limit: Option<u16>,
|
||||
shallow: Option<bool>,
|
||||
) -> Result<Array, JsValue> {
|
||||
// TODO: try later to return Vec<String> from async function.
|
||||
|
||||
if let Some(cursor) = cursor {
|
||||
return self
|
||||
.inner_list(url)?
|
||||
.reverse(reverse.unwrap_or(false))
|
||||
.limit(limit.unwrap_or(u16::MAX))
|
||||
.cursor(&cursor)
|
||||
.shallow(shallow.unwrap_or(false))
|
||||
.send()
|
||||
.await
|
||||
.map(|urls| {
|
||||
let js_array = Array::new();
|
||||
|
||||
for url in urls {
|
||||
js_array.push(&JsValue::from_str(&url));
|
||||
}
|
||||
|
||||
js_array
|
||||
})
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
|
||||
self.inner_list(url)?
|
||||
.reverse(reverse.unwrap_or(false))
|
||||
.limit(limit.unwrap_or(u16::MAX))
|
||||
.shallow(shallow.unwrap_or(false))
|
||||
.send()
|
||||
.await
|
||||
.map(|urls| {
|
||||
let js_array = Array::new();
|
||||
|
||||
for url in urls {
|
||||
js_array.push(&JsValue::from_str(&url));
|
||||
}
|
||||
|
||||
js_array
|
||||
})
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
113
pubky/src/wasm/api/auth.rs
Normal file
113
pubky/src/wasm/api/auth.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use url::Url;
|
||||
|
||||
use pubky_common::capabilities::Capabilities;
|
||||
|
||||
use crate::Error;
|
||||
use crate::PubkyClient;
|
||||
|
||||
use crate::wasm::wrappers::keys::{Keypair, PublicKey};
|
||||
use crate::wasm::wrappers::session::Session;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[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<Session, JsValue> {
|
||||
Ok(Session(
|
||||
self.inner_signup(keypair.as_inner(), homeserver.as_inner())
|
||||
.await
|
||||
.map_err(JsValue::from)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Check the current sesison for a given Pubky in its homeserver.
|
||||
///
|
||||
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
|
||||
/// or throws the recieved error if the response has any other `>=400` status code.
|
||||
#[wasm_bindgen]
|
||||
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>, JsValue> {
|
||||
self.inner_session(pubky.as_inner())
|
||||
.await
|
||||
.map(|s| s.map(Session))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Signout from a homeserver.
|
||||
#[wasm_bindgen]
|
||||
pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> {
|
||||
self.inner_signout(pubky.as_inner())
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Signin to a homeserver using the root Keypair.
|
||||
#[wasm_bindgen]
|
||||
pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> {
|
||||
self.inner_signin(keypair.as_inner())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
|
||||
/// verifying that AuthToken, and if capabilities were requested, signing in to
|
||||
/// the Pubky's homeserver and returning the [Session] information.
|
||||
///
|
||||
/// Returns a tuple of [pubkyAuthUrl, Promise<Session>]
|
||||
#[wasm_bindgen(js_name = "authRequest")]
|
||||
pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result<js_sys::Array, JsValue> {
|
||||
let mut relay: Url = relay
|
||||
.try_into()
|
||||
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
|
||||
|
||||
let (pubkyauth_url, client_secret) = self.create_auth_request(
|
||||
&mut relay,
|
||||
&Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?,
|
||||
)?;
|
||||
|
||||
let this = self.clone();
|
||||
|
||||
let future = async move {
|
||||
this.subscribe_to_auth_response(relay, &client_secret)
|
||||
.await
|
||||
.map(|pubky| JsValue::from(PublicKey(pubky)))
|
||||
.map_err(|err| JsValue::from_str(&format!("{:?}", err)))
|
||||
};
|
||||
|
||||
let promise = wasm_bindgen_futures::future_to_promise(future);
|
||||
|
||||
// Return the URL and the promise
|
||||
let js_tuple = js_sys::Array::new();
|
||||
js_tuple.push(&JsValue::from_str(pubkyauth_url.as_ref()));
|
||||
js_tuple.push(&promise);
|
||||
|
||||
Ok(js_tuple)
|
||||
}
|
||||
|
||||
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
|
||||
/// source of the pubkyauth request url.
|
||||
#[wasm_bindgen(js_name = "sendAuthToken")]
|
||||
pub async fn send_auth_token(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
pubkyauth_url: &str,
|
||||
) -> Result<(), JsValue> {
|
||||
let pubkyauth_url: Url = pubkyauth_url
|
||||
.try_into()
|
||||
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
|
||||
|
||||
self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
56
pubky/src/wasm/api/http.rs
Normal file
56
pubky/src/wasm/api/http.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
//! Fetch method handling HTTP and Pubky urls with Pkarr TLD.
|
||||
|
||||
use js_sys::Promise;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use reqwest::Url;
|
||||
|
||||
use crate::PubkyClient;
|
||||
|
||||
use super::super::internals::resolve;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl PubkyClient {
|
||||
#[wasm_bindgen]
|
||||
pub async fn fetch(
|
||||
&self,
|
||||
url: &str,
|
||||
init: &web_sys::RequestInit,
|
||||
) -> Result<js_sys::Promise, JsValue> {
|
||||
let mut url: Url = url.try_into().map_err(|err| {
|
||||
JsValue::from_str(&format!("PubkyClient::fetch(): Invalid `url`; {:?}", err))
|
||||
})?;
|
||||
|
||||
resolve(&self.pkarr, &mut url)
|
||||
.await
|
||||
.map_err(|err| JsValue::from_str(&format!("PubkyClient::fetch(): {:?}", err)))?;
|
||||
|
||||
let js_req =
|
||||
web_sys::Request::new_with_str_and_init(url.as_str(), init).map_err(|err| {
|
||||
JsValue::from_str(&format!("PubkyClient::fetch(): Invalid `init`; {:?}", err))
|
||||
})?;
|
||||
|
||||
Ok(js_fetch(&js_req))
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = fetch)]
|
||||
fn fetch_with_request(input: &web_sys::Request) -> Promise;
|
||||
}
|
||||
|
||||
fn js_fetch(req: &web_sys::Request) -> Promise {
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
let global = js_sys::global();
|
||||
|
||||
if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
|
||||
{
|
||||
global
|
||||
.unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
|
||||
.fetch_with_request(req)
|
||||
} else {
|
||||
// browser
|
||||
fetch_with_request(req)
|
||||
}
|
||||
}
|
||||
6
pubky/src/wasm/api/mod.rs
Normal file
6
pubky/src/wasm/api/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod http;
|
||||
pub mod recovery_file;
|
||||
|
||||
// TODO: put the Homeserver API behind a feature flag
|
||||
pub mod auth;
|
||||
pub mod public;
|
||||
87
pubky/src/wasm/api/public.rs
Normal file
87
pubky/src/wasm/api/public.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use js_sys::{Array, Uint8Array};
|
||||
|
||||
use crate::PubkyClient;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl PubkyClient {
|
||||
#[wasm_bindgen]
|
||||
/// Upload a small payload to a given path.
|
||||
pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> {
|
||||
self.inner_put(url, content).await.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Download a small payload from a given path relative to a pubky author.
|
||||
#[wasm_bindgen]
|
||||
pub async fn get(&self, url: &str) -> Result<Option<Uint8Array>, JsValue> {
|
||||
self.inner_get(url)
|
||||
.await
|
||||
.map(|b| b.map(|b| (&*b).into()))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Delete a file at a path relative to a pubky author.
|
||||
#[wasm_bindgen]
|
||||
pub async fn delete(&self, url: &str) -> Result<(), JsValue> {
|
||||
self.inner_delete(url).await.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Returns a list of Pubky urls (as strings).
|
||||
///
|
||||
/// - `url`: The Pubky url (string) to the directory you want to list its content.
|
||||
/// - `cursor`: Either a full `pubky://` Url (from previous list response),
|
||||
/// or a path (to a file or directory) relative to the `url`
|
||||
/// - `reverse`: List in reverse order
|
||||
/// - `limit` Limit the number of urls in the response
|
||||
/// - `shallow`: List directories and files, instead of flat list of files.
|
||||
#[wasm_bindgen]
|
||||
pub async fn list(
|
||||
&self,
|
||||
url: &str,
|
||||
cursor: Option<String>,
|
||||
reverse: Option<bool>,
|
||||
limit: Option<u16>,
|
||||
shallow: Option<bool>,
|
||||
) -> Result<Array, JsValue> {
|
||||
// TODO: try later to return Vec<String> from async function.
|
||||
|
||||
if let Some(cursor) = cursor {
|
||||
return self
|
||||
.inner_list(url)?
|
||||
.reverse(reverse.unwrap_or(false))
|
||||
.limit(limit.unwrap_or(u16::MAX))
|
||||
.cursor(&cursor)
|
||||
.shallow(shallow.unwrap_or(false))
|
||||
.send()
|
||||
.await
|
||||
.map(|urls| {
|
||||
let js_array = Array::new();
|
||||
|
||||
for url in urls {
|
||||
js_array.push(&JsValue::from_str(&url));
|
||||
}
|
||||
|
||||
js_array
|
||||
})
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
|
||||
self.inner_list(url)?
|
||||
.reverse(reverse.unwrap_or(false))
|
||||
.limit(limit.unwrap_or(u16::MAX))
|
||||
.shallow(shallow.unwrap_or(false))
|
||||
.send()
|
||||
.await
|
||||
.map(|urls| {
|
||||
let js_array = Array::new();
|
||||
|
||||
for url in urls {
|
||||
js_array.push(&JsValue::from_str(&url));
|
||||
}
|
||||
|
||||
js_array
|
||||
})
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
use super::keys::Keypair;
|
||||
use crate::wasm::wrappers::keys::Keypair;
|
||||
|
||||
/// Create a recovery file of the `keypair`, containing the secret key encrypted
|
||||
/// using the `passphrase`.
|
||||
@@ -1,40 +0,0 @@
|
||||
use crate::PubkyClient;
|
||||
|
||||
use reqwest::{Method, RequestBuilder, Response};
|
||||
use url::Url;
|
||||
|
||||
impl PubkyClient {
|
||||
pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder {
|
||||
let mut request = self.http.request(method, url).fetch_credentials_include();
|
||||
|
||||
for cookie in self.session_cookies.read().unwrap().iter() {
|
||||
request = request.header("Cookie", cookie);
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
// Support cookies for nodejs
|
||||
|
||||
pub(crate) fn store_session(&self, response: &Response) {
|
||||
if let Some(cookie) = response
|
||||
.headers()
|
||||
.get("set-cookie")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| s.split(';').next())
|
||||
{
|
||||
self.session_cookies
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(cookie.to_string());
|
||||
}
|
||||
}
|
||||
pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) {
|
||||
let key = pubky.to_string();
|
||||
|
||||
self.session_cookies
|
||||
.write()
|
||||
.unwrap()
|
||||
.retain(|cookie| !cookie.starts_with(&key));
|
||||
}
|
||||
}
|
||||
33
pubky/src/wasm/internals.rs
Normal file
33
pubky/src/wasm/internals.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use reqwest::{Method, RequestBuilder};
|
||||
use url::Url;
|
||||
|
||||
use pkarr::{EndpointResolver, PublicKey};
|
||||
|
||||
use crate::{error::Result, PubkyClient};
|
||||
|
||||
// TODO: remove expect
|
||||
pub async fn resolve(pkarr: &pkarr::Client, url: &mut Url) -> Result<()> {
|
||||
let qname = url.host_str().expect("URL TO HAVE A HOST!").to_string();
|
||||
|
||||
// If http and has a Pubky TLD, switch to socket addresses.
|
||||
if url.scheme() == "http" && PublicKey::try_from(qname.as_str()).is_ok() {
|
||||
let endpoint = pkarr.resolve_endpoint(&qname).await?;
|
||||
|
||||
if let Some(socket_address) = endpoint.to_socket_addrs().into_iter().next() {
|
||||
url.set_host(Some(&socket_address.to_string()))?;
|
||||
let _ = url.set_port(Some(socket_address.port()));
|
||||
} else if let Some(port) = endpoint.port() {
|
||||
url.set_host(Some(endpoint.target()))?;
|
||||
let _ = url.set_port(Some(port));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl PubkyClient {
|
||||
/// A wrapper around [reqwest::Client::request], with the same signature between native and wasm.
|
||||
pub(crate) async fn inner_request(&self, method: Method, url: Url) -> RequestBuilder {
|
||||
self.http.request(method, url).fetch_credentials_include()
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
use reqwest::StatusCode;
|
||||
|
||||
pub use pkarr::{PublicKey, SignedPacket};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::PubkyClient;
|
||||
|
||||
// TODO: Add an in memory cache of packets
|
||||
|
||||
impl PubkyClient {
|
||||
//TODO: migrate to pkarr::PkarrRelayClient
|
||||
pub(crate) async fn pkarr_resolve(
|
||||
&self,
|
||||
public_key: &PublicKey,
|
||||
) -> Result<Option<SignedPacket>> {
|
||||
//TODO: Allow multiple relays in parallel
|
||||
let relay = self.pkarr_relays.first().expect("initialized with relays");
|
||||
|
||||
let res = self
|
||||
.http
|
||||
.get(format!("{relay}/{}", public_key))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// TODO: guard against too large responses.
|
||||
let bytes = res.bytes().await?;
|
||||
|
||||
let existing = SignedPacket::from_relay_payload(public_key, &bytes)?;
|
||||
|
||||
Ok(Some(existing))
|
||||
}
|
||||
|
||||
pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> {
|
||||
let relay = self.pkarr_relays.first().expect("initialized with relays");
|
||||
|
||||
self.http
|
||||
.put(format!("{relay}/{}", signed_packet.public_key()))
|
||||
.body(signed_packet.to_relay_payload())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
pubky/src/wasm/wrappers/mod.rs
Normal file
5
pubky/src/wasm/wrappers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Wasm wrappers around structs that we need to be turned into Classes
|
||||
//! in JavaScript.
|
||||
|
||||
pub mod keys;
|
||||
pub mod session;
|
||||
Reference in New Issue
Block a user