feat(pubky): auth working everywhere except nodejs

This commit is contained in:
nazeh
2024-07-28 23:24:32 +03:00
parent 3cc81a5d0e
commit d050866cce
17 changed files with 291 additions and 248 deletions

21
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@@ -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<AppState>,
TypedHeader(user_agent): TypedHeader<UserAgent>,
cookies: Cookies,
pubky: Pubky,
uri: Uri,
body: Bytes,
) -> Result<impl IntoResponse> {
// 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<UserAgent>,
cookies: Cookies,
pubky: Pubky,
uri: Uri,
body: Bytes,
) -> Result<impl IntoResponse> {
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()?;

View File

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

View File

@@ -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<Error> for JsValue {

View File

@@ -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<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<()> {
self.inner_signin(keypair).await
}
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?)
}
pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder {
self.http.request(method, url)
}
}

View File

@@ -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<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?)
}
}
#[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()));
}
}
}

View File

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

View File

@@ -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<Session> {
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<Option<Session>> {
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() });
}

View File

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

View File

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

View File

@@ -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<Option<Session>, 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()
}
}

View File

@@ -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<Session, JsValue> {
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)?)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
use pubky_common::session;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Session(pub(crate) session::Session);