Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ resolver = "2"
version = "0.0.1"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.75"
19 changes: 19 additions & 0 deletions crates/wavekat-platform-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description = "Rust client for the WaveKat platform — auth, sessions, artifact
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository = "https://github.com/wavekat/wavekat-platform-client"
homepage = "https://github.com/wavekat/wavekat-platform-client"
documentation = "https://docs.rs/wavekat-platform-client"
Expand All @@ -13,8 +14,26 @@ categories = ["api-bindings", "web-programming::http-client"]
exclude = ["CHANGELOG.md"]

[dependencies]
# HTTP client. rustls-tls keeps us off OpenSSL across platforms; json + gzip
# match what wavekat-cli already vetted; stream is needed for get_stream_to.
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "gzip", "stream"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
url = "2"
rand = "0.8"
thiserror = "1"
# `time` for the handshake timeout; `sync` for the oneshot used to hand
# the token off the blocking accept thread. Deliberately NOT enabling
# `rt-multi-thread` or `macros` — let the consumer pick the runtime.
tokio = { version = "1", default-features = false, features = ["time", "sync", "rt"] }
futures-util = "0.3"

[dev-dependencies]
tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread", "time", "sync"] }
# `examples/smoke.rs` is a small CLI-shaped binary for the manual smoke
# test (see docs/01-initial-port.md). The library itself does not depend
# on `webbrowser` — the example just uses it for convenience.
webbrowser = "1"

[package.metadata.docs.rs]
all-features = true
Expand Down
112 changes: 112 additions & 0 deletions crates/wavekat-platform-client/examples/smoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Manual smoke test for the platform client. Not in CI — needs a
//! reachable platform and (for `login`) a human at a browser.
//!
//! Usage:
//!
//! cargo run --example smoke -- login
//! cargo run --example smoke -- whoami --token wkcli_xxx
//! cargo run --example smoke -- revoke --token wkcli_xxx
//!
//! The base URL defaults to `https://platform.wavekat.com`; override
//! with `--base-url` or `WK_BASE_URL`.

use std::env;
use std::process::ExitCode;

use wavekat_platform_client::{loopback_handshake, Client, HandshakeOptions, Token};

const DEFAULT_BASE_URL: &str = "https://platform.wavekat.com";

fn main() -> ExitCode {
let args: Vec<String> = env::args().skip(1).collect();
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(r) => r,
Err(e) => {
eprintln!("failed to start tokio runtime: {e}");
return ExitCode::from(1);
}
};
match rt.block_on(run(args)) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}

async fn run(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let mut iter = args.into_iter();
let cmd = iter
.next()
.ok_or("missing subcommand: login | whoami | revoke")?;

let mut token: Option<String> = None;
let mut base_url: Option<String> = None;
while let Some(flag) = iter.next() {
match flag.as_str() {
"--token" => token = iter.next(),
"--base-url" => base_url = iter.next(),
other => return Err(format!("unknown flag: {other}").into()),
}
}
let base_url = base_url
.or_else(|| env::var("WK_BASE_URL").ok())
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());

match cmd.as_str() {
"login" => login(&base_url).await,
"whoami" => {
let t = token.ok_or("whoami requires --token")?;
whoami(&base_url, t).await
}
"revoke" => {
let t = token.ok_or("revoke requires --token")?;
revoke(&base_url, t).await
}
other => Err(format!("unknown subcommand: {other}").into()),
}
}

async fn login(base_url: &str) -> Result<(), Box<dyn std::error::Error>> {
let pending = loopback_handshake(base_url, HandshakeOptions::default())?;
let url = pending.url().to_string();
println!("Opening {base_url} in your browser to sign in…");
if let Err(e) = webbrowser::open(&url) {
eprintln!("(couldn't open the browser automatically: {e})");
println!("Open this URL manually:\n {url}\n");
} else {
println!("If it didn't open, use:\n {url}\n");
}
println!("Waiting for the browser to redirect back (Ctrl-C to cancel)…");
let outcome = pending.wait().await?;
println!("Got token: {:?}", outcome.token);
if let Some(login) = &outcome.login {
println!("Login (echoed from platform): {login}");
}
let client = Client::new(base_url, outcome.token)?;
let me = client.whoami().await?;
println!("Signed in as {} ({}, role: {})", me.login, me.id, me.role);
Ok(())
}

async fn whoami(base_url: &str, token: String) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(base_url, Token::new(token))?;
let me = client.whoami().await?;
println!("login: {}", me.login);
println!("id: {}", me.id);
println!("name: {}", me.name.as_deref().unwrap_or("-"));
println!("email: {}", me.email.as_deref().unwrap_or("-"));
println!("role: {}", me.role);
Ok(())
}

async fn revoke(base_url: &str, token: String) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(base_url, Token::new(token))?;
client.revoke_current_token().await?;
println!("Token revoked.");
Ok(())
}
Loading