use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use base64::{Engine as _, engine::general_purpose::STANDARD};
use pubky_common::{capabilities::Capabilities, crypto::PublicKey, session::CookieSessionRecord};
use super::credential::CookieCredential;
use crate::actors::session::core::PubkySession;
use crate::actors::session::credential::SessionCredential;
use crate::errors::{AuthError, RequestError};
use crate::{PubkyHttpClient, Result, cross_log};
#[cfg(target_arch = "wasm32")]
pub(crate) async fn import_session(
export: &str,
client: Option<PubkyHttpClient>,
) -> Result<PubkySession> {
let client = match client {
Some(c) => c,
None => PubkyHttpClient::new()?,
};
let bytes = STANDARD
.decode(export)
.map_err(|e| RequestError::Validation {
message: format!("invalid session export: {e}"),
})?;
let record =
CookieSessionRecord::deserialize(&bytes).map_err(|e| RequestError::Validation {
message: format!("invalid session export: {e}"),
})?;
let user = record.public_key().clone();
let credential: Arc<dyn SessionCredential> =
Arc::new(CookieCredential::new(user, None, record, None));
let session = PubkySession::from_credential(client, Arc::clone(&credential));
session
.revalidate()
.await?
.ok_or(AuthError::RequestExpired)?;
cross_log!(info, "Rehydrated session");
Ok(session)
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(
clippy::unused_async,
reason = "keep async signature aligned with WASM build"
)]
pub(crate) async fn import_session(
_export: &str,
_client: Option<PubkyHttpClient>,
) -> Result<PubkySession> {
Err(RequestError::Validation {
message: "session import is only supported on WASM targets".into(),
}
.into())
}
pub(crate) async fn import_session_secret(
token: &str,
client: Option<PubkyHttpClient>,
) -> Result<PubkySession> {
let client = match client {
Some(c) => c,
None => PubkyHttpClient::new()?,
};
let (pk_str, cookie) = token
.split_once(':')
.ok_or_else(|| RequestError::Validation {
message: "invalid secret: expected `<pubkey>:<cookie>`".into(),
})?;
let public_key = PublicKey::try_from_z32(pk_str).map_err(|_err| RequestError::Validation {
message: "invalid public key".into(),
})?;
cross_log!(info, "Importing session secret for {}", public_key);
let placeholder = CookieSessionRecord::new(&public_key, Capabilities::default(), None);
let cookie_credential = CookieCredential::new(
public_key.clone(),
Some(cookie.to_string()),
placeholder,
None,
);
let credential: Arc<dyn SessionCredential> = Arc::new(cookie_credential);
let session = PubkySession::from_credential(client, Arc::clone(&credential));
session
.revalidate()
.await?
.ok_or(AuthError::RequestExpired)?;
cross_log!(
info,
"Successfully imported session secret for {}",
public_key
);
Ok(session)
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn session_from_secret_file(
secret_file_path: &std::path::Path,
client: Option<PubkyHttpClient>,
) -> Result<PubkySession> {
match secret_file_path.extension().and_then(|e| e.to_str()) {
Some("sess") => { }
Some("pkarr") => {
return Err(RequestError::Validation {
message: format!(
"refused to load `{}`: `.pkarr` is a keypair secret. \
Use `Keypair::from_secret_file` to load keys. \
Session secrets must use the `.sess` extension.",
secret_file_path.display()
),
}
.into());
}
Some(other) => {
return Err(RequestError::Validation {
message: format!(
"invalid session secret extension `.{other}` for `{}`; expected `.sess`",
secret_file_path.display()
),
}
.into());
}
None => {
return Err(RequestError::Validation {
message: format!(
"missing extension for `{}`; session secret files must end with `.sess`",
secret_file_path.display()
),
}
.into());
}
}
let token =
std::fs::read_to_string(secret_file_path).map_err(|e| RequestError::Validation {
message: format!("failed to read session secret file: {e}"),
})?;
cross_log!(
info,
"Loading session secret from {}",
secret_file_path.display()
);
import_session_secret(token.trim(), client).await
}