use rand::Rng;
use vta_sdk::client::VtaClient;
use vta_sdk::session::SessionStore;
pub type OnboardingError = Box<dyn std::error::Error>;
fn mint_ephemeral_did_key() -> (String, String) {
let mut seed = [0u8; 32];
rand::rng().fill_bytes(&mut seed);
let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
let public_key = signing_key.verifying_key().to_bytes();
let did = format!(
"did:key:{}",
vta_sdk::did_key::ed25519_multibase_pubkey(&public_key)
);
let private_key_multibase = multibase::encode(multibase::Base::Base58Btc, seed);
(did, private_key_multibase)
}
#[derive(Debug, Clone)]
pub struct OnboardingTicket {
ephemeral_did: String,
vta_did: String,
}
impl OnboardingTicket {
pub fn ephemeral_did(&self) -> &str {
&self.ephemeral_did
}
pub fn vta_did(&self) -> &str {
&self.vta_did
}
pub fn import_did_command(&self, context: &str) -> String {
format!(
"vta import-did --did {} --role application --context {}",
self.ephemeral_did, context
)
}
}
pub struct IntegrationOnboarding {
session: SessionStore,
session_key: String,
}
impl IntegrationOnboarding {
pub fn new(session: SessionStore, session_key: impl Into<String>) -> Self {
Self {
session,
session_key: session_key.into(),
}
}
pub fn with_default_backend(
service_name: &str,
sessions_dir: std::path::PathBuf,
session_key: impl Into<String>,
) -> Self {
Self::new(SessionStore::new(service_name, sessions_dir), session_key)
}
pub fn session_key(&self) -> &str {
&self.session_key
}
pub fn session_store(&self) -> &SessionStore {
&self.session
}
pub fn is_onboarded(&self) -> bool {
self.session.has_session(&self.session_key)
&& !self.session.has_pending_vta_binding(&self.session_key)
}
pub fn begin(&self, vta_did: &str) -> Result<OnboardingTicket, OnboardingError> {
let vta_did = vta_did.trim();
if !vta_did.starts_with("did:") {
return Err("VTA DID must start with `did:` (e.g. did:webvh:..., did:key:...)".into());
}
let (ephemeral_did, private_key_multibase) = mint_ephemeral_did_key();
self.session.store_pending_rotation(
&self.session_key,
&ephemeral_did,
&private_key_multibase,
vta_did,
)?;
Ok(OnboardingTicket {
ephemeral_did,
vta_did: vta_did.to_string(),
})
}
pub async fn connect(
&self,
url_override: Option<&str>,
mediator_did_hint: Option<&str>,
) -> Result<VtaClient, OnboardingError> {
self.session
.connect(&self.session_key, url_override, mediator_did_hint)
.await
}
pub async fn connect_rest(&self, base_url: &str) -> Result<String, OnboardingError> {
self.session
.ensure_authenticated(base_url, &self.session_key)
.await
}
pub fn logout(&self) {
self.session.logout(&self.session_key);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minted_did_is_did_key_and_seed_roundtrips() {
let (did, priv_mb) = mint_ephemeral_did_key();
assert!(did.starts_with("did:key:z"));
let (_, seed) = multibase::decode(&priv_mb).unwrap();
assert_eq!(seed.len(), 32, "Ed25519 seed must be 32 bytes");
let seed: [u8; 32] = seed.try_into().unwrap();
let signing = ed25519_dalek::SigningKey::from_bytes(&seed);
let rederived = format!(
"did:key:{}",
vta_sdk::did_key::ed25519_multibase_pubkey(&signing.verifying_key().to_bytes())
);
assert_eq!(rederived, did);
}
#[test]
fn minted_dids_are_unique() {
let (a, _) = mint_ephemeral_did_key();
let (b, _) = mint_ephemeral_did_key();
assert_ne!(a, b);
}
#[test]
fn begin_rejects_non_did_vta() {
let onboarding = IntegrationOnboarding::with_default_backend(
"vti-secrets-test",
std::env::temp_dir(),
"onboarding-test-rejects",
);
assert!(onboarding.begin("not-a-did").is_err());
}
#[test]
fn import_did_command_is_application_scoped() {
let ticket = OnboardingTicket {
ephemeral_did: "did:key:z6MkExample".to_string(),
vta_did: "did:webvh:example.com:vta".to_string(),
};
let cmd = ticket.import_did_command("ctx-1");
assert!(cmd.contains("--role application"));
assert!(cmd.contains("--did did:key:z6MkExample"));
assert!(cmd.contains("--context ctx-1"));
}
}