From de3ad8b51581882feae47eec23ef1889e20a3779 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:39:24 +0200 Subject: [PATCH 01/24] # This is a combination of 2 commits. # This is the 1st commit message: Started with token source # This is the commit message #2: Before http --- Cargo.lock | 7 +++++ Cargo.toml | 1 + examples/token_source/Cargo.toml | 8 +++++ examples/token_source/src/main.rs | 19 ++++++++++++ livekit/src/room/mod.rs | 1 + livekit/src/room/token_source/mod.rs | 46 ++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+) create mode 100644 examples/token_source/Cargo.toml create mode 100644 examples/token_source/src/main.rs create mode 100644 livekit/src/room/token_source/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5da102a03..a1a0d751a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7466,6 +7466,13 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "token_source" +version = "0.1.0" +dependencies = [ + "livekit", +] + [[package]] name = "tokio" version = "1.53.1" diff --git a/Cargo.toml b/Cargo.toml index e7e4565a9..864b44869 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "examples/save_to_disk", "examples/screensharing", "examples/send_bytes", + "examples/token_source", "examples/webhooks", ] diff --git a/examples/token_source/Cargo.toml b/examples/token_source/Cargo.toml new file mode 100644 index 000000000..34f16b8e7 --- /dev/null +++ b/examples/token_source/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "token_source" +version = "0.1.0" +edition.workspace = true +publish = false + +[dependencies] +livekit = { path = "../../livekit", features = ["rustls-tls-native-roots"]} \ No newline at end of file diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs new file mode 100644 index 000000000..6fa438155 --- /dev/null +++ b/examples/token_source/src/main.rs @@ -0,0 +1,19 @@ +use livekit::token_source::{TokenSourceLiteral, TokenSourceResponse}; + +fn main() { + println!("Hello, world!"); + let test = TokenSourceLiteral::new(TokenSourceResponse{ + server_url: "Hello Max".to_string(), + participant_token: "Hello Max".to_string() + }); + match test.result { + Ok(response) => { + let url = response.server_url; + let token = response.participant_token; + println!("The response is server_url: {url} and token: {token}"); + }, + Err(error) => { + println!("I got error {error}") + }, + } +} \ No newline at end of file diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 09f028912..58182da27 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -77,6 +77,7 @@ pub mod participant; pub mod publication; pub mod rpc; pub mod track; +pub mod token_source; pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs new file mode 100644 index 000000000..cd2cca9b5 --- /dev/null +++ b/livekit/src/room/token_source/mod.rs @@ -0,0 +1,46 @@ +pub struct TokenSourceLiteral { + result: TokenSourceResult +} + +impl TokenSourceLiteral { + pub fn new(response: TokenSourceResponse) -> TokenSourceLiteral { + TokenSourceLiteral { result: Ok(response) } + } + pub fn fetch(&self) -> &TokenSourceResult { &self.result } +} + +pub struct TokenSourceSandbox { + sandbox_id: String +} + +impl TokenSourceSandbox { + pub fn new(sandbox_id: String) -> TokenSourceSandbox { + TokenSourceSandbox { sandbox_id } + } + pub async fn fetch(&self) -> &TokenSourceResult { + + } +} + +// ================================================================================ + +pub struct TokenSourceResponse { + pub server_url: String, + pub participant_token: String, +} + +impl TokenSourceResponse { + pub fn new(server_url: String, participant_token: String) -> TokenSourceResponse { + TokenSourceResponse{server_url: server_url, participant_token: participant_token} + } +} + +pub type TokenSourceResult = Result; + + #[derive(Debug, thiserror::Error)] + pub enum TokenSourceError { + #[error("error A occurred")] + ErrorA, + #[error("error B occurred")] + ErrorB, + } \ No newline at end of file From 62acd1e7d6a5e7f79de351637352a6268144c8ad Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:33:01 +0200 Subject: [PATCH 02/24] Started with token source Before http Moved everything into a modular crate Without borrow seems to work Token source with tokio async Move reqwest into workspace Token fetching from sandbox works TokenSourceEndpoint works the same as sandbox Using composition for sandbox Error status codes are working Remove room mod change Version 1 done, works like Unity First round of review changes done Switching to livekit-net crate instead of reqwest Revert putting reqwest dependency in workspace Remove test errors Renaming to development token server --- Cargo.lock | 15 ++- Cargo.toml | 2 + examples/token_source/Cargo.toml | 3 +- examples/token_source/src/main.rs | 49 ++++++-- livekit-token-source/Cargo.toml | 34 +++++ livekit-token-source/README.md | 0 livekit-token-source/src/error.rs | 14 +++ livekit-token-source/src/lib.rs | 12 ++ livekit-token-source/src/request.rs | 154 +++++++++++++++++++++++ livekit-token-source/src/response.rs | 9 ++ livekit-token-source/src/token_source.rs | 76 +++++++++++ livekit-token-source/tests/mock_http.rs | 130 +++++++++++++++++++ livekit/src/room/mod.rs | 1 - livekit/src/room/token_source/mod.rs | 46 ------- 14 files changed, 488 insertions(+), 57 deletions(-) create mode 100644 livekit-token-source/Cargo.toml create mode 100644 livekit-token-source/README.md create mode 100644 livekit-token-source/src/error.rs create mode 100644 livekit-token-source/src/lib.rs create mode 100644 livekit-token-source/src/request.rs create mode 100644 livekit-token-source/src/response.rs create mode 100644 livekit-token-source/src/token_source.rs create mode 100644 livekit-token-source/tests/mock_http.rs delete mode 100644 livekit/src/room/token_source/mod.rs diff --git a/Cargo.lock b/Cargo.lock index a1a0d751a..a1c767c97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4103,6 +4103,18 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "livekit-token-source" +version = "0.1.0" +dependencies = [ + "async-trait", + "livekit-net", + "serde", + "serde_json", + "thiserror 2.0.19", + "tokio", +] + [[package]] name = "livekit-uniffi" version = "0.1.8" @@ -7470,7 +7482,8 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "token_source" version = "0.1.0" dependencies = [ - "livekit", + "livekit-token-source", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 864b44869..a89e7b76e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "livekit-ffi", "livekit-uniffi", "livekit-datatrack", + "livekit-token-source", "livekit-ffi-node-bindings", "livekit-net", "livekit-runtime", @@ -54,6 +55,7 @@ livekit = { version = "0.8.2", path = "livekit" } livekit-api = { version = "0.6.2", path = "livekit-api" } livekit-ffi = { version = "0.12.74", path = "livekit-ffi" } livekit-datatrack = { version = "0.1.13", path = "livekit-datatrack" } +livekit-token-source = { version = "0.1.0", path = "livekit-token-source" } livekit-common = { version = "0.1.1", path = "livekit-common" } livekit-data-stream = { version = "0.1.2", path = "livekit-data-stream" } livekit-net = { version = "0.1.2", path = "livekit-net" } diff --git a/examples/token_source/Cargo.toml b/examples/token_source/Cargo.toml index 34f16b8e7..bfb72b035 100644 --- a/examples/token_source/Cargo.toml +++ b/examples/token_source/Cargo.toml @@ -5,4 +5,5 @@ edition.workspace = true publish = false [dependencies] -livekit = { path = "../../livekit", features = ["rustls-tls-native-roots"]} \ No newline at end of file +livekit-token-source = { version = "0.1.0", path = "../../livekit-token-source" } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } \ No newline at end of file diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index 6fa438155..49105229e 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -1,16 +1,49 @@ -use livekit::token_source::{TokenSourceLiteral, TokenSourceResponse}; +use livekit_token_source::{TokenSourceEndpoint, TokenSourceLiteral, TokenSourceResponse, TokenSourceDevelopmentTokenServer, TokenSourceFetchOptions}; -fn main() { - println!("Hello, world!"); - let test = TokenSourceLiteral::new(TokenSourceResponse{ - server_url: "Hello Max".to_string(), - participant_token: "Hello Max".to_string() +#[tokio::main] +async fn main() { + // ======================================================= + let literal = TokenSourceLiteral::new(TokenSourceResponse{ + server_url: "< some server url >".to_string(), + participant_token: "< some token >\n".to_string() }); - match test.result { + match literal.fetch() { + Ok(response) => { + let url = &response.server_url; + let token = &response.participant_token; + println!("From Literal: {url} and token: {token}"); + }, + Err(error) => { + println!("I got error {error}") + }, + } + + let options = TokenSourceFetchOptions::new() + .with_agent_name("Church"); + + // ======================================================= + let development_token_server = TokenSourceDevelopmentTokenServer::new("test1-xqsb8v".to_string()); + match development_token_server.fetch(&options).await { + Ok(response) => { + let url = response.server_url; + let token = response.participant_token; + println!("From Development Token Server: {url} and token: {token}\n"); + }, + Err(error) => { + println!("I got error {error}") + }, + } + + // ======================================================= + let endpoint = TokenSourceEndpoint::new( + "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", + vec![("X-Sandbox-ID".to_string(), "test1-xqsb8v".to_string())] + ); + match endpoint.fetch(&options).await { Ok(response) => { let url = response.server_url; let token = response.participant_token; - println!("The response is server_url: {url} and token: {token}"); + println!("From Endpoint: {url} and token: {token}\n"); }, Err(error) => { println!("I got error {error}") diff --git a/livekit-token-source/Cargo.toml b/livekit-token-source/Cargo.toml new file mode 100644 index 000000000..2a812775f --- /dev/null +++ b/livekit-token-source/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "livekit-token-source" +description = "Token sources for the LiveKit Rust SDK" +version = "0.1.0" +license.workspace = true +edition.workspace = true +repository.workspace = true +readme = "README.md" + +[features] +default = ["native-tokio", "rustls-tls-native-roots"] + +# Backend bundles — pass-throughs to livekit-net. With none enabled the crate is +# backend-blind: the host must register a client via `livekit_net::set_http_client`. +native-tokio = ["livekit-net/native-tokio"] +native-async = ["livekit-net/native-async"] +native-dispatcher = ["livekit-net/native-dispatcher"] + +# TLS pass-throughs (only meaningful with a native backend). See livekit-api's +# Cargo.toml for guidance on choosing one, notably in container deployments. +native-tls = ["livekit-net/native-tls"] +native-tls-vendored = ["livekit-net/native-tls-vendored"] +rustls-tls-native-roots = ["livekit-net/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["livekit-net/rustls-tls-webpki-roots"] + +[dependencies] +livekit-net = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +async-trait = "0.1" +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/livekit-token-source/README.md b/livekit-token-source/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/livekit-token-source/src/error.rs b/livekit-token-source/src/error.rs new file mode 100644 index 000000000..71a35c311 --- /dev/null +++ b/livekit-token-source/src/error.rs @@ -0,0 +1,14 @@ +#[derive(Debug, thiserror::Error)] +pub enum TokenSourceError { + #[error("no HTTP client available; enable a livekit-net backend feature or call livekit_net::set_http_client")] + TransportNotConfigured, + + #[error("failed to fetch token: {0}")] + Transport(#[from] livekit_net::TransportError), + + #[error("failed to serialize request / parse response: {0}")] + Json(#[from] serde_json::Error), + + #[error("token server returned {status}: {body}")] + Server{ status: u16, body: String }, +} diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs new file mode 100644 index 000000000..2f8b8d5cc --- /dev/null +++ b/livekit-token-source/src/lib.rs @@ -0,0 +1,12 @@ +mod error; +mod request; +mod response; +mod token_source; + +pub use error::TokenSourceError; +pub use response::TokenSourceResponse; +pub use response::TokenSourceResult; +pub use request::TokenSourceFetchOptions; +pub use token_source::TokenSourceLiteral; +pub use token_source::TokenSourceEndpoint; +pub use token_source::TokenSourceDevelopmentTokenServer; \ No newline at end of file diff --git a/livekit-token-source/src/request.rs b/livekit-token-source/src/request.rs new file mode 100644 index 000000000..ee9d164ed --- /dev/null +++ b/livekit-token-source/src/request.rs @@ -0,0 +1,154 @@ +use std::collections::HashMap; + +/// Per-call overrides used to parameterize a token request. +/// +/// Every option is optional: anything left unset is omitted from the request and the +/// server picks a default. Set only the options you care about: +/// +/// ``` +/// # use livekit_token_source::TokenSourceFetchOptions; +/// let options = TokenSourceFetchOptions::new() +/// .with_room_name("my-room") +/// .with_participant_identity("user-123"); +/// ``` +#[derive(Default, Clone, Debug)] +pub struct TokenSourceFetchOptions { + pub(crate) room_name: Option, + pub(crate) participant_name: Option, + pub(crate) participant_identity: Option, + pub(crate) participant_metadata: Option, + pub(crate) participant_attributes: Option>, + pub(crate) agent_name: Option, + pub(crate) agent_metadata: Option, + pub(crate) agent_deployment: Option, +} + +impl TokenSourceFetchOptions { + /// Creates empty fetch options; the server picks a default for every field. + pub fn new() -> Self { + Self::default() + } + + /// Sets the name of the room being requested when generating credentials. + pub fn with_room_name(mut self, value: impl Into) -> Self { + self.room_name = Some(value.into()); + self + } + + /// Sets the name of the participant being requested when generating credentials. + pub fn with_participant_name(mut self, value: impl Into) -> Self { + self.participant_name = Some(value.into()); + self + } + + /// Sets the identity of the participant being requested when generating credentials. + pub fn with_participant_identity(mut self, value: impl Into) -> Self { + self.participant_identity = Some(value.into()); + self + } + + /// Sets the metadata of the participant being requested when generating credentials. + pub fn with_participant_metadata(mut self, value: impl Into) -> Self { + self.participant_metadata = Some(value.into()); + self + } + + /// Adds the given attributes to the participant attributes, keeping any set previously. + /// A key that was already set is overwritten with its new value. + pub fn with_participant_attributes(mut self, value: HashMap) -> Self { + self.participant_attributes.get_or_insert_with(HashMap::new).extend(value); + self + } + + /// Adds a single attribute to the participant attributes, keeping any set previously. + pub fn with_participant_attribute( + mut self, + key: impl Into, + value: impl Into, + ) -> Self { + self.participant_attributes + .get_or_insert_with(HashMap::new) + .insert(key.into(), value.into()); + self + } + + /// Sets the name of the agent to dispatch into the room. + pub fn with_agent_name(mut self, value: impl Into) -> Self { + self.agent_name = Some(value.into()); + self + } + + /// Sets the metadata to pass to the dispatched agent. + pub fn with_agent_metadata(mut self, value: impl Into) -> Self { + self.agent_metadata = Some(value.into()); + self + } + + /// Sets the deployment to target. Leave unset to target the production deployment. + pub fn with_agent_deployment(mut self, value: impl Into) -> Self { + self.agent_deployment = Some(value.into()); + self + } +} + +/// The JSON body posted to the token endpoint. Built from [`TokenSourceFetchOptions`]; +/// the flat agent fields get nested under `room_config.agents` to match the server's schema. +#[derive(serde::Serialize)] +pub(crate) struct TokenSourceRequest { + #[serde(skip_serializing_if = "Option::is_none")] + room_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + participant_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + participant_identity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + participant_metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + participant_attributes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + room_config: Option, +} + +#[derive(serde::Serialize)] +struct RoomConfig { + agents: Vec, +} + +#[derive(serde::Serialize)] +struct AgentDispatch { + #[serde(skip_serializing_if = "Option::is_none")] + agent_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + deployment: Option, +} + +impl From<&TokenSourceFetchOptions> for TokenSourceRequest { + fn from(options: &TokenSourceFetchOptions) -> TokenSourceRequest { + // Only include a room_config when at least one agent field is set. + let room_config = if options.agent_name.is_some() + || options.agent_metadata.is_some() + || options.agent_deployment.is_some() + { + Some(RoomConfig { + agents: vec![AgentDispatch { + agent_name: options.agent_name.clone(), + metadata: options.agent_metadata.clone(), + deployment: options.agent_deployment.clone(), + }], + }) + } else { + None + }; + + TokenSourceRequest { + room_name: options.room_name.clone(), + participant_name: options.participant_name.clone(), + participant_identity: options.participant_identity.clone(), + participant_metadata: options.participant_metadata.clone(), + participant_attributes: options.participant_attributes.clone(), + room_config, + } + } +} diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs new file mode 100644 index 000000000..a713f5d92 --- /dev/null +++ b/livekit-token-source/src/response.rs @@ -0,0 +1,9 @@ +use crate::error::TokenSourceError; + +#[derive(Debug, serde::Deserialize)] +pub struct TokenSourceResponse { + pub server_url: String, + pub participant_token: String, +} + +pub type TokenSourceResult = Result; \ No newline at end of file diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs new file mode 100644 index 000000000..a829717ca --- /dev/null +++ b/livekit-token-source/src/token_source.rs @@ -0,0 +1,76 @@ +use crate::request::TokenSourceRequest; +use crate::request::TokenSourceFetchOptions; +use crate::response::TokenSourceResponse; +use crate::response::TokenSourceResult; +use crate::error::TokenSourceError; +use livekit_net::{Header, HttpClientExt}; + +const DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL: &str = "https://cloud-api.livekit.io/api/v2/sandbox/connection-details"; +const DEVELOPMENT_TOKEN_SERVER_ID_HEADER: &str = "X-Sandbox-ID"; + +pub struct TokenSourceLiteral { + result: TokenSourceResult +} + +impl TokenSourceLiteral { + pub fn new(response: TokenSourceResponse) -> TokenSourceLiteral { + TokenSourceLiteral { result: Ok(response) } + } + pub fn fetch(&self) -> &TokenSourceResult { &self.result } +} + +pub struct TokenSourceEndpoint { + endpoint_url: String, + headers: Vec<(String, String)>, +} + +impl TokenSourceEndpoint { + pub fn new(endpoint_url: impl Into, headers: Vec<(String, String)>) -> TokenSourceEndpoint { + TokenSourceEndpoint{ + endpoint_url: endpoint_url.into(), + headers, + } + } + + pub async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { + let request = TokenSourceRequest::from(options); + + let http_client = livekit_net::http_client().ok_or(TokenSourceError::TransportNotConfigured)?; + + let body = serde_json::to_vec(&request)?; + let mut headers = vec![Header { name: "Content-Type".into(), value: "application/json".into() }]; + headers.extend(self.headers.iter().map(|(name, value)| Header { name: name.clone(), value: value.clone() })); + + let response = http_client.post(self.endpoint_url.clone(), headers, body).await?; + + if !(200..300).contains(&response.status) { + return Err(TokenSourceError::Server { + status: response.status, + body: String::from_utf8_lossy(&response.body).into_owned() + }); + } + + let connection_details = serde_json::from_slice::(&response.body)?; + Ok(connection_details) + } +} + +pub struct TokenSourceDevelopmentTokenServer { + token_source_endpoint: TokenSourceEndpoint +} + +impl TokenSourceDevelopmentTokenServer { + pub fn new(token_server_id: String) -> TokenSourceDevelopmentTokenServer { + let token_source_endpoint = TokenSourceEndpoint::new( + DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL, + vec![(DEVELOPMENT_TOKEN_SERVER_ID_HEADER.to_string(), token_server_id)] + ); + + TokenSourceDevelopmentTokenServer { + token_source_endpoint + } + } + pub async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { + self.token_source_endpoint.fetch(options).await + } +} \ No newline at end of file diff --git a/livekit-token-source/tests/mock_http.rs b/livekit-token-source/tests/mock_http.rs new file mode 100644 index 000000000..d52ee1e38 --- /dev/null +++ b/livekit-token-source/tests/mock_http.rs @@ -0,0 +1,130 @@ +//! Tests `TokenSourceEndpoint::fetch` against a mock `livekit_net::HttpClient`. +//! +//! The livekit-net registry is process-wide and first-set-wins, so every test +//! lives in this one file and registration goes through a `Once`. Each test uses +//! a distinct endpoint URL; the mock dispatches its response on the URL and +//! records each request under it, so concurrently running tests never collide. + +use livekit_net::{Header, HttpMethod, HttpResponse, TransportError}; +use livekit_token_source::{TokenSourceEndpoint, TokenSourceError, TokenSourceFetchOptions}; +use std::collections::HashMap; +use std::sync::{Mutex, Once}; + +#[derive(Clone)] +struct Captured { + method: HttpMethod, + headers: Vec
, + body: Option>, +} + +static CAPTURED: Mutex>> = Mutex::new(None); + +fn captured(url: &str) -> Captured { + CAPTURED.lock().unwrap().as_ref().unwrap().get(url).expect("request not captured").clone() +} + +struct MockHttp; + +#[async_trait::async_trait] +impl livekit_net::HttpClient for MockHttp { + async fn request( + &self, + method: HttpMethod, + url: String, + headers: Vec
, + body: Option>, + ) -> Result { + CAPTURED + .lock() + .unwrap() + .get_or_insert_with(HashMap::new) + .insert(url.clone(), Captured { method, headers, body }); + + if url.contains("server-error") { + return Ok(HttpResponse { status: 500, headers: vec![], body: b"boom".to_vec() }); + } + if url.contains("badjson") { + return Ok(HttpResponse { + status: 200, + headers: vec![], + body: b"this is not json".to_vec(), + }); + } + if url.contains("connrefused") { + return Err(TransportError::Connection("connection refused".into())); + } + let body = br#"{"server_url":"wss://mock.livekit.cloud","participant_token":"tok-123"}"#; + Ok(HttpResponse { status: 200, headers: vec![], body: body.to_vec() }) + } +} + +static INSTALL: Once = Once::new(); + +fn install_mock() { + INSTALL.call_once(|| livekit_net::set_http_client(std::sync::Arc::new(MockHttp))); +} + +fn header<'a>(headers: &'a [Header], name: &str) -> Option<&'a str> { + headers.iter().find(|h| h.name.eq_ignore_ascii_case(name)).map(|h| h.value.as_str()) +} + +#[tokio::test] +async fn fetch_posts_json_and_parses_response() { + install_mock(); + let url = "https://token.test/ok"; + let endpoint = TokenSourceEndpoint::new( + url, + vec![("X-Sandbox-ID".to_string(), "sandbox-42".to_string())], + ); + let options = TokenSourceFetchOptions::new() + .with_room_name("my-room") + .with_participant_identity("user-123"); + + let response = endpoint.fetch(&options).await.expect("fetch should succeed"); + assert_eq!(response.server_url, "wss://mock.livekit.cloud"); + assert_eq!(response.participant_token, "tok-123"); + + let req = captured(url); + assert_eq!(req.method, HttpMethod::Post); + assert_eq!(header(&req.headers, "Content-Type"), Some("application/json")); + assert_eq!(header(&req.headers, "X-Sandbox-ID"), Some("sandbox-42")); + + let body: serde_json::Value = serde_json::from_slice(&req.body.expect("body")).unwrap(); + assert_eq!(body["room_name"], "my-room"); + assert_eq!(body["participant_identity"], "user-123"); + // Unset options must be omitted, not sent as null. + assert!(body.get("participant_name").is_none()); +} + +#[tokio::test] +async fn non_2xx_maps_to_server_error() { + install_mock(); + let endpoint = TokenSourceEndpoint::new("https://token.test/server-error", vec![]); + + let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); + match err { + TokenSourceError::Server { status, body } => { + assert_eq!(status, 500); + assert_eq!(body, "boom"); + } + other => panic!("expected Server error, got {other:?}"), + } +} + +#[tokio::test] +async fn invalid_json_maps_to_json_error() { + install_mock(); + let endpoint = TokenSourceEndpoint::new("https://token.test/badjson", vec![]); + + let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); + assert!(matches!(err, TokenSourceError::Json(_)), "expected Json error, got {err:?}"); +} + +#[tokio::test] +async fn transport_error_maps_to_transport_variant() { + install_mock(); + let endpoint = TokenSourceEndpoint::new("https://token.test/connrefused", vec![]); + + let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); + assert!(matches!(err, TokenSourceError::Transport(_)), "expected Transport error, got {err:?}"); +} diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 58182da27..09f028912 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -77,7 +77,6 @@ pub mod participant; pub mod publication; pub mod rpc; pub mod track; -pub mod token_source; pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/livekit/src/room/token_source/mod.rs b/livekit/src/room/token_source/mod.rs deleted file mode 100644 index cd2cca9b5..000000000 --- a/livekit/src/room/token_source/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -pub struct TokenSourceLiteral { - result: TokenSourceResult -} - -impl TokenSourceLiteral { - pub fn new(response: TokenSourceResponse) -> TokenSourceLiteral { - TokenSourceLiteral { result: Ok(response) } - } - pub fn fetch(&self) -> &TokenSourceResult { &self.result } -} - -pub struct TokenSourceSandbox { - sandbox_id: String -} - -impl TokenSourceSandbox { - pub fn new(sandbox_id: String) -> TokenSourceSandbox { - TokenSourceSandbox { sandbox_id } - } - pub async fn fetch(&self) -> &TokenSourceResult { - - } -} - -// ================================================================================ - -pub struct TokenSourceResponse { - pub server_url: String, - pub participant_token: String, -} - -impl TokenSourceResponse { - pub fn new(server_url: String, participant_token: String) -> TokenSourceResponse { - TokenSourceResponse{server_url: server_url, participant_token: participant_token} - } -} - -pub type TokenSourceResult = Result; - - #[derive(Debug, thiserror::Error)] - pub enum TokenSourceError { - #[error("error A occurred")] - ErrorA, - #[error("error B occurred")] - ErrorB, - } \ No newline at end of file From 7aed81a01df9c7bdf25aae66c0af4c2187f90c77 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:54:06 +0200 Subject: [PATCH 03/24] Using static factory pattern --- examples/token_source/src/main.rs | 8 ++--- livekit-token-source/src/lib.rs | 1 + livekit-token-source/src/token_source.rs | 44 +++++++++++++----------- livekit-token-source/tests/mock_http.rs | 10 +++--- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index 49105229e..c13caba90 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -1,9 +1,9 @@ -use livekit_token_source::{TokenSourceEndpoint, TokenSourceLiteral, TokenSourceResponse, TokenSourceDevelopmentTokenServer, TokenSourceFetchOptions}; +use livekit_token_source::{TokenSource, TokenSourceResponse, TokenSourceFetchOptions}; #[tokio::main] async fn main() { // ======================================================= - let literal = TokenSourceLiteral::new(TokenSourceResponse{ + let literal = TokenSource::literal(TokenSourceResponse{ server_url: "< some server url >".to_string(), participant_token: "< some token >\n".to_string() }); @@ -22,7 +22,7 @@ async fn main() { .with_agent_name("Church"); // ======================================================= - let development_token_server = TokenSourceDevelopmentTokenServer::new("test1-xqsb8v".to_string()); + let development_token_server = TokenSource::development_token_server("test1-xqsb8v".to_string()); match development_token_server.fetch(&options).await { Ok(response) => { let url = response.server_url; @@ -35,7 +35,7 @@ async fn main() { } // ======================================================= - let endpoint = TokenSourceEndpoint::new( + let endpoint = TokenSource::endpoint( "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", vec![("X-Sandbox-ID".to_string(), "test1-xqsb8v".to_string())] ); diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index 2f8b8d5cc..00258a155 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -7,6 +7,7 @@ pub use error::TokenSourceError; pub use response::TokenSourceResponse; pub use response::TokenSourceResult; pub use request::TokenSourceFetchOptions; +pub use token_source::TokenSource; pub use token_source::TokenSourceLiteral; pub use token_source::TokenSourceEndpoint; pub use token_source::TokenSourceDevelopmentTokenServer; \ No newline at end of file diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index a829717ca..165f1c2f9 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -8,14 +8,35 @@ use livekit_net::{Header, HttpClientExt}; const DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL: &str = "https://cloud-api.livekit.io/api/v2/sandbox/connection-details"; const DEVELOPMENT_TOKEN_SERVER_ID_HEADER: &str = "X-Sandbox-ID"; +pub enum TokenSource {} + +impl TokenSource { + pub fn literal(response: TokenSourceResponse) -> TokenSourceLiteral { + TokenSourceLiteral { result: Ok(response) } + } + + pub fn endpoint(endpoint_url: impl Into, headers: Vec<(String, String)>) -> TokenSourceEndpoint { + TokenSourceEndpoint { + endpoint_url: endpoint_url.into(), + headers, + } + } + + pub fn development_token_server(token_server_id: String) -> TokenSourceDevelopmentTokenServer { + TokenSourceDevelopmentTokenServer { + token_source_endpoint: TokenSource::endpoint( + DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL, + vec![(DEVELOPMENT_TOKEN_SERVER_ID_HEADER.to_string(), token_server_id)], + ), + } + } +} + pub struct TokenSourceLiteral { result: TokenSourceResult } impl TokenSourceLiteral { - pub fn new(response: TokenSourceResponse) -> TokenSourceLiteral { - TokenSourceLiteral { result: Ok(response) } - } pub fn fetch(&self) -> &TokenSourceResult { &self.result } } @@ -25,13 +46,6 @@ pub struct TokenSourceEndpoint { } impl TokenSourceEndpoint { - pub fn new(endpoint_url: impl Into, headers: Vec<(String, String)>) -> TokenSourceEndpoint { - TokenSourceEndpoint{ - endpoint_url: endpoint_url.into(), - headers, - } - } - pub async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { let request = TokenSourceRequest::from(options); @@ -60,16 +74,6 @@ pub struct TokenSourceDevelopmentTokenServer { } impl TokenSourceDevelopmentTokenServer { - pub fn new(token_server_id: String) -> TokenSourceDevelopmentTokenServer { - let token_source_endpoint = TokenSourceEndpoint::new( - DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL, - vec![(DEVELOPMENT_TOKEN_SERVER_ID_HEADER.to_string(), token_server_id)] - ); - - TokenSourceDevelopmentTokenServer { - token_source_endpoint - } - } pub async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { self.token_source_endpoint.fetch(options).await } diff --git a/livekit-token-source/tests/mock_http.rs b/livekit-token-source/tests/mock_http.rs index d52ee1e38..51d6df4b0 100644 --- a/livekit-token-source/tests/mock_http.rs +++ b/livekit-token-source/tests/mock_http.rs @@ -6,7 +6,7 @@ //! records each request under it, so concurrently running tests never collide. use livekit_net::{Header, HttpMethod, HttpResponse, TransportError}; -use livekit_token_source::{TokenSourceEndpoint, TokenSourceError, TokenSourceFetchOptions}; +use livekit_token_source::{TokenSource, TokenSourceError, TokenSourceFetchOptions}; use std::collections::HashMap; use std::sync::{Mutex, Once}; @@ -72,7 +72,7 @@ fn header<'a>(headers: &'a [Header], name: &str) -> Option<&'a str> { async fn fetch_posts_json_and_parses_response() { install_mock(); let url = "https://token.test/ok"; - let endpoint = TokenSourceEndpoint::new( + let endpoint = TokenSource::endpoint( url, vec![("X-Sandbox-ID".to_string(), "sandbox-42".to_string())], ); @@ -99,7 +99,7 @@ async fn fetch_posts_json_and_parses_response() { #[tokio::test] async fn non_2xx_maps_to_server_error() { install_mock(); - let endpoint = TokenSourceEndpoint::new("https://token.test/server-error", vec![]); + let endpoint = TokenSource::endpoint("https://token.test/server-error", vec![]); let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); match err { @@ -114,7 +114,7 @@ async fn non_2xx_maps_to_server_error() { #[tokio::test] async fn invalid_json_maps_to_json_error() { install_mock(); - let endpoint = TokenSourceEndpoint::new("https://token.test/badjson", vec![]); + let endpoint = TokenSource::endpoint("https://token.test/badjson", vec![]); let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); assert!(matches!(err, TokenSourceError::Json(_)), "expected Json error, got {err:?}"); @@ -123,7 +123,7 @@ async fn invalid_json_maps_to_json_error() { #[tokio::test] async fn transport_error_maps_to_transport_variant() { install_mock(); - let endpoint = TokenSourceEndpoint::new("https://token.test/connrefused", vec![]); + let endpoint = TokenSource::endpoint("https://token.test/connrefused", vec![]); let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); assert!(matches!(err, TokenSourceError::Transport(_)), "expected Transport error, got {err:?}"); From 887c54396fedb8223be050f4ea51747c97d0df34 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:45:19 +0200 Subject: [PATCH 04/24] Add license headers --- livekit-token-source/src/error.rs | 14 ++++++++++++++ livekit-token-source/src/lib.rs | 14 ++++++++++++++ livekit-token-source/src/request.rs | 14 ++++++++++++++ livekit-token-source/src/response.rs | 14 ++++++++++++++ livekit-token-source/src/token_source.rs | 14 ++++++++++++++ livekit-token-source/tests/mock_http.rs | 15 +++++++++++++++ 6 files changed, 85 insertions(+) diff --git a/livekit-token-source/src/error.rs b/livekit-token-source/src/error.rs index 71a35c311..2eb2f5f99 100644 --- a/livekit-token-source/src/error.rs +++ b/livekit-token-source/src/error.rs @@ -1,3 +1,17 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #[derive(Debug, thiserror::Error)] pub enum TokenSourceError { #[error("no HTTP client available; enable a livekit-net backend feature or call livekit_net::set_http_client")] diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index 00258a155..6d308c23e 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -1,3 +1,17 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + mod error; mod request; mod response; diff --git a/livekit-token-source/src/request.rs b/livekit-token-source/src/request.rs index ee9d164ed..6fc0137a3 100644 --- a/livekit-token-source/src/request.rs +++ b/livekit-token-source/src/request.rs @@ -1,3 +1,17 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use std::collections::HashMap; /// Per-call overrides used to parameterize a token request. diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs index a713f5d92..9d46bed96 100644 --- a/livekit-token-source/src/response.rs +++ b/livekit-token-source/src/response.rs @@ -1,3 +1,17 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use crate::error::TokenSourceError; #[derive(Debug, serde::Deserialize)] diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index 165f1c2f9..ed318848e 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -1,3 +1,17 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use crate::request::TokenSourceRequest; use crate::request::TokenSourceFetchOptions; use crate::response::TokenSourceResponse; diff --git a/livekit-token-source/tests/mock_http.rs b/livekit-token-source/tests/mock_http.rs index 51d6df4b0..742bab60f 100644 --- a/livekit-token-source/tests/mock_http.rs +++ b/livekit-token-source/tests/mock_http.rs @@ -1,3 +1,18 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + //! Tests `TokenSourceEndpoint::fetch` against a mock `livekit_net::HttpClient`. //! //! The livekit-net registry is process-wide and first-set-wins, so every test From 549af2ae6b04fdb548c071b42b8f6d767c4a0ca9 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:03:29 +0200 Subject: [PATCH 05/24] Adding to knope.toml --- knope.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/knope.toml b/knope.toml index 4e8783902..1644a3e4d 100644 --- a/knope.toml +++ b/knope.toml @@ -180,4 +180,12 @@ versioned_files = [ "Cargo.lock", { path = "Cargo.toml", dependency = "livekit-runtime" }, ] -changelog = "livekit-runtime/CHANGELOG.md" \ No newline at end of file +changelog = "livekit-runtime/CHANGELOG.md" + +[packages.livekit-token-source] +versioned_files = [ + "livekit-token-source/Cargo.toml", + "Cargo.lock", + { path = "Cargo.toml", dependency = "livekit-net" }, +] +changelog = "livekit-token-source/CHANGELOG.md" \ No newline at end of file From e0ee66794ce24f66e81fb8bc9fead7b760c3d985 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:05:35 +0200 Subject: [PATCH 06/24] Changeset --- .changeset/add_a_tokensource_crate_to_the_rust_sdks.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add_a_tokensource_crate_to_the_rust_sdks.md diff --git a/.changeset/add_a_tokensource_crate_to_the_rust_sdks.md b/.changeset/add_a_tokensource_crate_to_the_rust_sdks.md new file mode 100644 index 000000000..e4593b92a --- /dev/null +++ b/.changeset/add_a_tokensource_crate_to_the_rust_sdks.md @@ -0,0 +1,5 @@ +--- +livekit-token-source: patch +--- + +Add a TokenSource crate to the Rust SDKs - #1274 (@MaxHeimbrock) From ac7a5d4e039553a49614dd0563c306161db1c42e Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:52:31 +0200 Subject: [PATCH 07/24] fix(knope): point livekit-token-source at its own root Cargo.toml pin The versioned_files dependency entry named livekit-net, so releasing livekit-token-source would have rewritten the livekit-net version pin in the workspace Cargo.toml instead of its own. Co-Authored-By: Claude Fable 5 --- knope.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/knope.toml b/knope.toml index 1644a3e4d..2855e2f69 100644 --- a/knope.toml +++ b/knope.toml @@ -186,6 +186,6 @@ changelog = "livekit-runtime/CHANGELOG.md" versioned_files = [ "livekit-token-source/Cargo.toml", "Cargo.lock", - { path = "Cargo.toml", dependency = "livekit-net" }, + { path = "Cargo.toml", dependency = "livekit-token-source" }, ] -changelog = "livekit-token-source/CHANGELOG.md" \ No newline at end of file +changelog = "livekit-token-source/CHANGELOG.md" From 64a70f11db261e4d6098885dc1aaf33c739f3d51 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:53:19 +0200 Subject: [PATCH 08/24] style: run cargo fmt on livekit-token-source and example Co-Authored-By: Claude Fable 5 --- examples/token_source/src/main.rs | 30 +++++++------- livekit-token-source/src/error.rs | 2 +- livekit-token-source/src/lib.rs | 6 +-- livekit-token-source/src/response.rs | 2 +- livekit-token-source/src/token_source.rs | 51 +++++++++++++++--------- livekit-token-source/tests/mock_http.rs | 7 +--- 6 files changed, 55 insertions(+), 43 deletions(-) diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index c13caba90..2cd686111 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -1,52 +1,52 @@ -use livekit_token_source::{TokenSource, TokenSourceResponse, TokenSourceFetchOptions}; +use livekit_token_source::{TokenSource, TokenSourceFetchOptions, TokenSourceResponse}; #[tokio::main] async fn main() { // ======================================================= - let literal = TokenSource::literal(TokenSourceResponse{ + let literal = TokenSource::literal(TokenSourceResponse { server_url: "< some server url >".to_string(), - participant_token: "< some token >\n".to_string() + participant_token: "< some token >\n".to_string(), }); match literal.fetch() { Ok(response) => { let url = &response.server_url; let token = &response.participant_token; println!("From Literal: {url} and token: {token}"); - }, + } Err(error) => { println!("I got error {error}") - }, + } } - let options = TokenSourceFetchOptions::new() - .with_agent_name("Church"); + let options = TokenSourceFetchOptions::new().with_agent_name("Church"); // ======================================================= - let development_token_server = TokenSource::development_token_server("test1-xqsb8v".to_string()); + let development_token_server = + TokenSource::development_token_server("test1-xqsb8v".to_string()); match development_token_server.fetch(&options).await { Ok(response) => { let url = response.server_url; let token = response.participant_token; println!("From Development Token Server: {url} and token: {token}\n"); - }, + } Err(error) => { println!("I got error {error}") - }, + } } // ======================================================= let endpoint = TokenSource::endpoint( - "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", - vec![("X-Sandbox-ID".to_string(), "test1-xqsb8v".to_string())] + "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", + vec![("X-Sandbox-ID".to_string(), "test1-xqsb8v".to_string())], ); match endpoint.fetch(&options).await { Ok(response) => { let url = response.server_url; let token = response.participant_token; println!("From Endpoint: {url} and token: {token}\n"); - }, + } Err(error) => { println!("I got error {error}") - }, + } } -} \ No newline at end of file +} diff --git a/livekit-token-source/src/error.rs b/livekit-token-source/src/error.rs index 2eb2f5f99..0a09834db 100644 --- a/livekit-token-source/src/error.rs +++ b/livekit-token-source/src/error.rs @@ -24,5 +24,5 @@ pub enum TokenSourceError { Json(#[from] serde_json::Error), #[error("token server returned {status}: {body}")] - Server{ status: u16, body: String }, + Server { status: u16, body: String }, } diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index 6d308c23e..53235f9a3 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -18,10 +18,10 @@ mod response; mod token_source; pub use error::TokenSourceError; +pub use request::TokenSourceFetchOptions; pub use response::TokenSourceResponse; pub use response::TokenSourceResult; -pub use request::TokenSourceFetchOptions; pub use token_source::TokenSource; -pub use token_source::TokenSourceLiteral; +pub use token_source::TokenSourceDevelopmentTokenServer; pub use token_source::TokenSourceEndpoint; -pub use token_source::TokenSourceDevelopmentTokenServer; \ No newline at end of file +pub use token_source::TokenSourceLiteral; diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs index 9d46bed96..87546219b 100644 --- a/livekit-token-source/src/response.rs +++ b/livekit-token-source/src/response.rs @@ -20,4 +20,4 @@ pub struct TokenSourceResponse { pub participant_token: String, } -pub type TokenSourceResult = Result; \ No newline at end of file +pub type TokenSourceResult = Result; diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index ed318848e..ffa1de5dc 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::request::TokenSourceRequest; +use crate::error::TokenSourceError; use crate::request::TokenSourceFetchOptions; +use crate::request::TokenSourceRequest; use crate::response::TokenSourceResponse; use crate::response::TokenSourceResult; -use crate::error::TokenSourceError; use livekit_net::{Header, HttpClientExt}; -const DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL: &str = "https://cloud-api.livekit.io/api/v2/sandbox/connection-details"; +const DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL: &str = + "https://cloud-api.livekit.io/api/v2/sandbox/connection-details"; const DEVELOPMENT_TOKEN_SERVER_ID_HEADER: &str = "X-Sandbox-ID"; pub enum TokenSource {} @@ -29,11 +30,11 @@ impl TokenSource { TokenSourceLiteral { result: Ok(response) } } - pub fn endpoint(endpoint_url: impl Into, headers: Vec<(String, String)>) -> TokenSourceEndpoint { - TokenSourceEndpoint { - endpoint_url: endpoint_url.into(), - headers, - } + pub fn endpoint( + endpoint_url: impl Into, + headers: Vec<(String, String)>, + ) -> TokenSourceEndpoint { + TokenSourceEndpoint { endpoint_url: endpoint_url.into(), headers } } pub fn development_token_server(token_server_id: String) -> TokenSourceDevelopmentTokenServer { @@ -47,11 +48,13 @@ impl TokenSource { } pub struct TokenSourceLiteral { - result: TokenSourceResult + result: TokenSourceResult, } impl TokenSourceLiteral { - pub fn fetch(&self) -> &TokenSourceResult { &self.result } + pub fn fetch(&self) -> &TokenSourceResult { + &self.result + } } pub struct TokenSourceEndpoint { @@ -60,21 +63,30 @@ pub struct TokenSourceEndpoint { } impl TokenSourceEndpoint { - pub async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { + pub async fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult { let request = TokenSourceRequest::from(options); - let http_client = livekit_net::http_client().ok_or(TokenSourceError::TransportNotConfigured)?; + let http_client = + livekit_net::http_client().ok_or(TokenSourceError::TransportNotConfigured)?; let body = serde_json::to_vec(&request)?; - let mut headers = vec![Header { name: "Content-Type".into(), value: "application/json".into() }]; - headers.extend(self.headers.iter().map(|(name, value)| Header { name: name.clone(), value: value.clone() })); + let mut headers = + vec![Header { name: "Content-Type".into(), value: "application/json".into() }]; + headers.extend( + self.headers + .iter() + .map(|(name, value)| Header { name: name.clone(), value: value.clone() }), + ); let response = http_client.post(self.endpoint_url.clone(), headers, body).await?; if !(200..300).contains(&response.status) { return Err(TokenSourceError::Server { status: response.status, - body: String::from_utf8_lossy(&response.body).into_owned() + body: String::from_utf8_lossy(&response.body).into_owned(), }); } @@ -84,11 +96,14 @@ impl TokenSourceEndpoint { } pub struct TokenSourceDevelopmentTokenServer { - token_source_endpoint: TokenSourceEndpoint + token_source_endpoint: TokenSourceEndpoint, } impl TokenSourceDevelopmentTokenServer { - pub async fn fetch(&self, options: &TokenSourceFetchOptions) -> TokenSourceResult { + pub async fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult { self.token_source_endpoint.fetch(options).await } -} \ No newline at end of file +} diff --git a/livekit-token-source/tests/mock_http.rs b/livekit-token-source/tests/mock_http.rs index 742bab60f..3b0ce2aad 100644 --- a/livekit-token-source/tests/mock_http.rs +++ b/livekit-token-source/tests/mock_http.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. - //! Tests `TokenSourceEndpoint::fetch` against a mock `livekit_net::HttpClient`. //! //! The livekit-net registry is process-wide and first-set-wins, so every test @@ -87,10 +86,8 @@ fn header<'a>(headers: &'a [Header], name: &str) -> Option<&'a str> { async fn fetch_posts_json_and_parses_response() { install_mock(); let url = "https://token.test/ok"; - let endpoint = TokenSource::endpoint( - url, - vec![("X-Sandbox-ID".to_string(), "sandbox-42".to_string())], - ); + let endpoint = + TokenSource::endpoint(url, vec![("X-Sandbox-ID".to_string(), "sandbox-42".to_string())]); let options = TokenSourceFetchOptions::new() .with_room_name("my-room") .with_participant_identity("user-123"); From 227ee41bb481a20101ca60bf66b6c96b6f8f8e6d Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:55:05 +0200 Subject: [PATCH 09/24] feat(token-source): add TokenSourceFixed and TokenSourceConfigurable traits Mirrors the JS SDK's TokenSourceFixed / TokenSourceConfigurable split: fixed sources fetch without options, configurable sources take TokenSourceFetchOptions. Endpoint and DevelopmentTokenServer implement Configurable; Literal implements Fixed and now stores the response directly, removing the never-constructed Err arm. This gives generic call sites (a future Room::connect) and custom credential backends a common interface. Co-Authored-By: Claude Fable 5 --- examples/token_source/src/main.rs | 7 ++-- livekit-token-source/Cargo.toml | 2 +- livekit-token-source/src/lib.rs | 2 ++ livekit-token-source/src/response.rs | 2 +- livekit-token-source/src/token_source.rs | 43 +++++++++++++++++++----- livekit-token-source/tests/mock_http.rs | 4 ++- 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index 2cd686111..bbcdbd773 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -1,4 +1,7 @@ -use livekit_token_source::{TokenSource, TokenSourceFetchOptions, TokenSourceResponse}; +use livekit_token_source::{ + TokenSource, TokenSourceConfigurable, TokenSourceFetchOptions, TokenSourceFixed, + TokenSourceResponse, +}; #[tokio::main] async fn main() { @@ -7,7 +10,7 @@ async fn main() { server_url: "< some server url >".to_string(), participant_token: "< some token >\n".to_string(), }); - match literal.fetch() { + match literal.fetch().await { Ok(response) => { let url = &response.server_url; let token = &response.participant_token; diff --git a/livekit-token-source/Cargo.toml b/livekit-token-source/Cargo.toml index 2a812775f..71be6b507 100644 --- a/livekit-token-source/Cargo.toml +++ b/livekit-token-source/Cargo.toml @@ -24,11 +24,11 @@ rustls-tls-native-roots = ["livekit-net/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["livekit-net/rustls-tls-webpki-roots"] [dependencies] +async-trait = "0.1" livekit-net = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -async-trait = "0.1" tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index 53235f9a3..d8457d766 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -22,6 +22,8 @@ pub use request::TokenSourceFetchOptions; pub use response::TokenSourceResponse; pub use response::TokenSourceResult; pub use token_source::TokenSource; +pub use token_source::TokenSourceConfigurable; pub use token_source::TokenSourceDevelopmentTokenServer; pub use token_source::TokenSourceEndpoint; +pub use token_source::TokenSourceFixed; pub use token_source::TokenSourceLiteral; diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs index 87546219b..ba7bb61f2 100644 --- a/livekit-token-source/src/response.rs +++ b/livekit-token-source/src/response.rs @@ -14,7 +14,7 @@ use crate::error::TokenSourceError; -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, Clone, serde::Deserialize)] pub struct TokenSourceResponse { pub server_url: String, pub participant_token: String, diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index ffa1de5dc..256801ddc 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -17,17 +17,39 @@ use crate::request::TokenSourceFetchOptions; use crate::request::TokenSourceRequest; use crate::response::TokenSourceResponse; use crate::response::TokenSourceResult; +use async_trait::async_trait; use livekit_net::{Header, HttpClientExt}; const DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL: &str = "https://cloud-api.livekit.io/api/v2/sandbox/connection-details"; const DEVELOPMENT_TOKEN_SERVER_ID_HEADER: &str = "X-Sandbox-ID"; +/// A token source whose credentials are not parameterized: `fetch` takes no +/// options and every call resolves the same way. +#[async_trait] +pub trait TokenSourceFixed { + async fn fetch(&self) -> TokenSourceResult; +} + +/// A token source that generates credentials from per-call +/// [`TokenSourceFetchOptions`] (room name, participant identity, agent +/// dispatch, ...). +/// +/// Implement this trait to plug a custom credential backend into code that is +/// generic over token sources. +#[async_trait] +pub trait TokenSourceConfigurable { + async fn fetch( + &self, + options: &TokenSourceFetchOptions, + ) -> TokenSourceResult; +} + pub enum TokenSource {} impl TokenSource { pub fn literal(response: TokenSourceResponse) -> TokenSourceLiteral { - TokenSourceLiteral { result: Ok(response) } + TokenSourceLiteral { response } } pub fn endpoint( @@ -48,12 +70,13 @@ impl TokenSource { } pub struct TokenSourceLiteral { - result: TokenSourceResult, + response: TokenSourceResponse, } -impl TokenSourceLiteral { - pub fn fetch(&self) -> &TokenSourceResult { - &self.result +#[async_trait] +impl TokenSourceFixed for TokenSourceLiteral { + async fn fetch(&self) -> TokenSourceResult { + Ok(self.response.clone()) } } @@ -62,8 +85,9 @@ pub struct TokenSourceEndpoint { headers: Vec<(String, String)>, } -impl TokenSourceEndpoint { - pub async fn fetch( +#[async_trait] +impl TokenSourceConfigurable for TokenSourceEndpoint { + async fn fetch( &self, options: &TokenSourceFetchOptions, ) -> TokenSourceResult { @@ -99,8 +123,9 @@ pub struct TokenSourceDevelopmentTokenServer { token_source_endpoint: TokenSourceEndpoint, } -impl TokenSourceDevelopmentTokenServer { - pub async fn fetch( +#[async_trait] +impl TokenSourceConfigurable for TokenSourceDevelopmentTokenServer { + async fn fetch( &self, options: &TokenSourceFetchOptions, ) -> TokenSourceResult { diff --git a/livekit-token-source/tests/mock_http.rs b/livekit-token-source/tests/mock_http.rs index 3b0ce2aad..f5a972b76 100644 --- a/livekit-token-source/tests/mock_http.rs +++ b/livekit-token-source/tests/mock_http.rs @@ -20,7 +20,9 @@ //! records each request under it, so concurrently running tests never collide. use livekit_net::{Header, HttpMethod, HttpResponse, TransportError}; -use livekit_token_source::{TokenSource, TokenSourceError, TokenSourceFetchOptions}; +use livekit_token_source::{ + TokenSource, TokenSourceConfigurable, TokenSourceError, TokenSourceFetchOptions, +}; use std::collections::HashMap; use std::sync::{Mutex, Once}; From 6c140b5ebe6f0293ef09236d57a6f365246e4364 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:55:35 +0200 Subject: [PATCH 10/24] refactor(token-source): rename agent_deployment option to deployment Matches the JS SDK's TokenSourceFetchOptions field name; the wire mapping (room_config.agents[0].deployment) is unchanged. Co-Authored-By: Claude Fable 5 --- livekit-token-source/src/request.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/livekit-token-source/src/request.rs b/livekit-token-source/src/request.rs index 6fc0137a3..6391a2356 100644 --- a/livekit-token-source/src/request.rs +++ b/livekit-token-source/src/request.rs @@ -34,7 +34,7 @@ pub struct TokenSourceFetchOptions { pub(crate) participant_attributes: Option>, pub(crate) agent_name: Option, pub(crate) agent_metadata: Option, - pub(crate) agent_deployment: Option, + pub(crate) deployment: Option, } impl TokenSourceFetchOptions { @@ -98,9 +98,9 @@ impl TokenSourceFetchOptions { self } - /// Sets the deployment to target. Leave unset to target the production deployment. - pub fn with_agent_deployment(mut self, value: impl Into) -> Self { - self.agent_deployment = Some(value.into()); + /// Sets the agent deployment to target. Leave unset to target the production deployment. + pub fn with_deployment(mut self, value: impl Into) -> Self { + self.deployment = Some(value.into()); self } } @@ -143,13 +143,13 @@ impl From<&TokenSourceFetchOptions> for TokenSourceRequest { // Only include a room_config when at least one agent field is set. let room_config = if options.agent_name.is_some() || options.agent_metadata.is_some() - || options.agent_deployment.is_some() + || options.deployment.is_some() { Some(RoomConfig { agents: vec![AgentDispatch { agent_name: options.agent_name.clone(), metadata: options.agent_metadata.clone(), - deployment: options.agent_deployment.clone(), + deployment: options.deployment.clone(), }], }) } else { From 9d09a40684b384d8abe0392f8015e8d5222f224a Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:56:07 +0200 Subject: [PATCH 11/24] refactor(token-source): accept impl Into for token server id, tolerate camelCase responses The documented endpoint contract is snake_case; serde aliases add the same leniency the JS SDK gets from proto3 fromJson, which also accepts camelCase field names. Co-Authored-By: Claude Fable 5 --- livekit-token-source/src/response.rs | 4 ++++ livekit-token-source/src/token_source.rs | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs index ba7bb61f2..276692d64 100644 --- a/livekit-token-source/src/response.rs +++ b/livekit-token-source/src/response.rs @@ -16,7 +16,11 @@ use crate::error::TokenSourceError; #[derive(Debug, Clone, serde::Deserialize)] pub struct TokenSourceResponse { + // The documented endpoint contract is snake_case; the camelCase aliases + // match the leniency of the JS SDK, which parses via proto3 fromJson. + #[serde(alias = "serverUrl")] pub server_url: String, + #[serde(alias = "participantToken")] pub participant_token: String, } diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index 256801ddc..1a8094658 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -59,11 +59,13 @@ impl TokenSource { TokenSourceEndpoint { endpoint_url: endpoint_url.into(), headers } } - pub fn development_token_server(token_server_id: String) -> TokenSourceDevelopmentTokenServer { + pub fn development_token_server( + token_server_id: impl Into, + ) -> TokenSourceDevelopmentTokenServer { TokenSourceDevelopmentTokenServer { token_source_endpoint: TokenSource::endpoint( DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL, - vec![(DEVELOPMENT_TOKEN_SERVER_ID_HEADER.to_string(), token_server_id)], + vec![(DEVELOPMENT_TOKEN_SERVER_ID_HEADER.to_string(), token_server_id.into())], ), } } From b1184d6f6ff338f258b5db01cf7b44b9f9d2b52f Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:57:21 +0200 Subject: [PATCH 12/24] docs(token-source): document the public API Ports the JS SDK doc comments, including the production-use warning on the development token server, and adds crate-level docs. Co-Authored-By: Claude Fable 5 --- livekit-token-source/src/lib.rs | 7 +++++++ livekit-token-source/src/response.rs | 3 +++ livekit-token-source/src/token_source.rs | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index d8457d766..c5232c13c 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -12,6 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Token sources for the LiveKit Rust SDK. +//! +//! A token source procures the credentials — server URL and participant +//! token — needed to join a LiveKit room. Construct one via the +//! [`TokenSource`] factory functions, or implement [`TokenSourceFixed`] / +//! [`TokenSourceConfigurable`] to plug in a custom credential backend. + mod error; mod request; mod response; diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs index 276692d64..a93c228af 100644 --- a/livekit-token-source/src/response.rs +++ b/livekit-token-source/src/response.rs @@ -14,6 +14,8 @@ use crate::error::TokenSourceError; +/// The credentials returned by a token source: the server to connect to and +/// the participant token to authenticate with. #[derive(Debug, Clone, serde::Deserialize)] pub struct TokenSourceResponse { // The documented endpoint contract is snake_case; the camelCase aliases @@ -24,4 +26,5 @@ pub struct TokenSourceResponse { pub participant_token: String, } +/// Result alias used by all token source operations. pub type TokenSourceResult = Result; diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index 1a8094658..79be5eccc 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -45,13 +45,25 @@ pub trait TokenSourceConfigurable { ) -> TokenSourceResult; } +/// Factory for the token sources shipped with this crate. Not instantiable; +/// use the associated functions to construct a concrete source. pub enum TokenSource {} impl TokenSource { + /// Creates a token source holding a single, literal set of credentials, + /// returned as-is on every fetch. pub fn literal(response: TokenSourceResponse) -> TokenSourceLiteral { TokenSourceLiteral { response } } + /// Creates a token source that fetches credentials from the given URL + /// using the standard token endpoint format. + /// + /// The given headers are sent along with every request, e.g. for + /// authentication against the endpoint. + /// + /// See + /// for the endpoint contract. pub fn endpoint( endpoint_url: impl Into, headers: Vec<(String, String)>, @@ -59,6 +71,13 @@ impl TokenSource { TokenSourceEndpoint { endpoint_url: endpoint_url.into(), headers } } + /// Creates a token source that queries a LiveKit development token server + /// for credentials, for quick prototyping / getting-started use cases. + /// + /// **This token provider is INSECURE and should NOT be used in + /// production.** + /// + /// See . pub fn development_token_server( token_server_id: impl Into, ) -> TokenSourceDevelopmentTokenServer { @@ -71,6 +90,7 @@ impl TokenSource { } } +/// The return type of [`TokenSource::literal`]. pub struct TokenSourceLiteral { response: TokenSourceResponse, } @@ -82,6 +102,7 @@ impl TokenSourceFixed for TokenSourceLiteral { } } +/// The return type of [`TokenSource::endpoint`]. pub struct TokenSourceEndpoint { endpoint_url: String, headers: Vec<(String, String)>, @@ -121,6 +142,7 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { } } +/// The return type of [`TokenSource::development_token_server`]. pub struct TokenSourceDevelopmentTokenServer { token_source_endpoint: TokenSourceEndpoint, } From 5f01afa6e0a2e7ced152186721fd551034d8c344 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:57:58 +0200 Subject: [PATCH 13/24] build(token-source): drop TLS from default features Repo convention (livekit, livekit-api) is to ship no TLS in defaults and let consumers opt in; the example now enables rustls-tls-native-roots explicitly and uses the workspace dependency like the other examples. Co-Authored-By: Claude Fable 5 --- examples/token_source/Cargo.toml | 2 +- livekit-token-source/Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/token_source/Cargo.toml b/examples/token_source/Cargo.toml index bfb72b035..0c055f92f 100644 --- a/examples/token_source/Cargo.toml +++ b/examples/token_source/Cargo.toml @@ -5,5 +5,5 @@ edition.workspace = true publish = false [dependencies] -livekit-token-source = { version = "0.1.0", path = "../../livekit-token-source" } +livekit-token-source = { workspace = true, features = ["rustls-tls-native-roots"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } \ No newline at end of file diff --git a/livekit-token-source/Cargo.toml b/livekit-token-source/Cargo.toml index 71be6b507..f10499286 100644 --- a/livekit-token-source/Cargo.toml +++ b/livekit-token-source/Cargo.toml @@ -8,7 +8,8 @@ repository.workspace = true readme = "README.md" [features] -default = ["native-tokio", "rustls-tls-native-roots"] +# By default no TLS is enabled; pick one of the TLS features below. +default = ["native-tokio"] # Backend bundles — pass-throughs to livekit-net. With none enabled the crate is # backend-blind: the host must register a client via `livekit_net::set_http_client`. From ef3e095d6de65e7a89fd708fd6c3e16f7d74a09e Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:58:44 +0200 Subject: [PATCH 14/24] chore(token-source): clean up example, read sandbox id from env Removes the committed live sandbox id in favor of LIVEKIT_SANDBOX_ID and tidies the output. Co-Authored-By: Claude Fable 5 --- examples/token_source/src/main.rs | 66 +++++++++++++++---------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index bbcdbd773..1bbe4265b 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -5,51 +5,51 @@ use livekit_token_source::{ #[tokio::main] async fn main() { - // ======================================================= + // A literal token source returns a fixed set of pre-provisioned credentials. let literal = TokenSource::literal(TokenSourceResponse { - server_url: "< some server url >".to_string(), - participant_token: "< some token >\n".to_string(), + server_url: "wss://example.livekit.cloud".to_string(), + participant_token: "".to_string(), }); match literal.fetch().await { - Ok(response) => { - let url = &response.server_url; - let token = &response.participant_token; - println!("From Literal: {url} and token: {token}"); - } - Err(error) => { - println!("I got error {error}") - } + Ok(response) => println!( + "literal: server_url={} participant_token={}", + response.server_url, response.participant_token + ), + Err(error) => eprintln!("literal fetch failed: {error}"), } - let options = TokenSourceFetchOptions::new().with_agent_name("Church"); + // The remaining sources query LiveKit's development token server, which + // requires the ID of a sandbox created in your LiveKit Cloud project. + let Ok(sandbox_id) = std::env::var("LIVEKIT_SANDBOX_ID") else { + eprintln!("set LIVEKIT_SANDBOX_ID to run the remaining examples"); + return; + }; - // ======================================================= - let development_token_server = - TokenSource::development_token_server("test1-xqsb8v".to_string()); + let options = TokenSourceFetchOptions::new() + .with_room_name("example-room") + .with_participant_identity("example-user"); + + // Development token server: for prototyping only, NOT for production use. + let development_token_server = TokenSource::development_token_server(sandbox_id.clone()); match development_token_server.fetch(&options).await { - Ok(response) => { - let url = response.server_url; - let token = response.participant_token; - println!("From Development Token Server: {url} and token: {token}\n"); - } - Err(error) => { - println!("I got error {error}") - } + Ok(response) => println!( + "development token server: server_url={} participant_token={}", + response.server_url, response.participant_token + ), + Err(error) => eprintln!("development token server fetch failed: {error}"), } - // ======================================================= + // Endpoint: POSTs the fetch options to a token endpoint using the standard + // format; here pointed at the same development token server. let endpoint = TokenSource::endpoint( "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", - vec![("X-Sandbox-ID".to_string(), "test1-xqsb8v".to_string())], + vec![("X-Sandbox-ID".to_string(), sandbox_id)], ); match endpoint.fetch(&options).await { - Ok(response) => { - let url = response.server_url; - let token = response.participant_token; - println!("From Endpoint: {url} and token: {token}\n"); - } - Err(error) => { - println!("I got error {error}") - } + Ok(response) => println!( + "endpoint: server_url={} participant_token={}", + response.server_url, response.participant_token + ), + Err(error) => eprintln!("endpoint fetch failed: {error}"), } } From a62d679c228fd30c9e138d625f9b2bdbc2379679 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:59:14 +0200 Subject: [PATCH 15/24] test(token-source): cover agent options to room_config request nesting Co-Authored-By: Claude Fable 5 --- livekit-token-source/tests/mock_http.rs | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/livekit-token-source/tests/mock_http.rs b/livekit-token-source/tests/mock_http.rs index f5a972b76..08a1b21e1 100644 --- a/livekit-token-source/tests/mock_http.rs +++ b/livekit-token-source/tests/mock_http.rs @@ -134,6 +134,40 @@ async fn invalid_json_maps_to_json_error() { assert!(matches!(err, TokenSourceError::Json(_)), "expected Json error, got {err:?}"); } +#[tokio::test] +async fn agent_options_nest_under_room_config() { + install_mock(); + let url = "https://token.test/agent"; + let endpoint = TokenSource::endpoint(url, vec![]); + let options = TokenSourceFetchOptions::new() + .with_agent_name("my-agent") + .with_agent_metadata("meta") + .with_deployment("staging"); + + endpoint.fetch(&options).await.expect("fetch should succeed"); + + let req = captured(url); + let body: serde_json::Value = serde_json::from_slice(&req.body.expect("body")).unwrap(); + let agent = &body["room_config"]["agents"][0]; + assert_eq!(agent["agent_name"], "my-agent"); + assert_eq!(agent["metadata"], "meta"); + assert_eq!(agent["deployment"], "staging"); +} + +#[tokio::test] +async fn room_config_is_omitted_without_agent_options() { + install_mock(); + let url = "https://token.test/no-agent"; + let endpoint = TokenSource::endpoint(url, vec![]); + let options = TokenSourceFetchOptions::new().with_room_name("plain-room"); + + endpoint.fetch(&options).await.expect("fetch should succeed"); + + let req = captured(url); + let body: serde_json::Value = serde_json::from_slice(&req.body.expect("body")).unwrap(); + assert!(body.get("room_config").is_none()); +} + #[tokio::test] async fn transport_error_maps_to_transport_variant() { install_mock(); From 4ce03861a459596e6e608099bd78b0666abfc04f Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:00:05 +0200 Subject: [PATCH 16/24] docs(token-source): write the crate README Cargo.toml declares readme = "README.md" but the file was empty. Co-Authored-By: Claude Fable 5 --- livekit-token-source/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/livekit-token-source/README.md b/livekit-token-source/README.md index e69de29bb..d878ddefc 100644 --- a/livekit-token-source/README.md +++ b/livekit-token-source/README.md @@ -0,0 +1,29 @@ +# LiveKit Token Source + +Token sources for the LiveKit Rust SDK. A token source procures the credentials — server URL and +participant token — needed to join a LiveKit room. + +Three sources ship with the crate, constructed via the `TokenSource` factory functions: + +- `TokenSource::literal` — a fixed set of pre-provisioned credentials. +- `TokenSource::endpoint` — fetches credentials from a token endpoint implementing the + [standard format](https://docs.livekit.io/frontends/build/authentication/endpoint/). +- `TokenSource::development_token_server` — queries a LiveKit + [development token server](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) + for prototyping. **Not for production use.** + +Custom credential backends can implement the `TokenSourceFixed` or `TokenSourceConfigurable` +traits. + +```rust +use livekit_token_source::{TokenSource, TokenSourceConfigurable, TokenSourceFetchOptions}; + +let source = TokenSource::endpoint("https://example.com/api/token", vec![]); +let options = TokenSourceFetchOptions::new() + .with_room_name("my-room") + .with_participant_identity("user-123"); +let response = source.fetch(&options).await?; +// connect with response.server_url / response.participant_token +``` + +See [examples/token_source](../examples/token_source) for a runnable example. From 5a431bd3e0a4178e7a59505863bd84c9388dde24 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:00:29 +0200 Subject: [PATCH 17/24] chore: add code owner for livekit-token-source Co-Authored-By: Claude Fable 5 --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1b64e56e4..2921eb74d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,5 @@ /livekit-datatrack/ @ladvoc @1egoman /livekit-data-stream/ @ladvoc @1egoman /livekit-wakeword/ @pham-tuan-binh -/livekit-net/ @jhugman \ No newline at end of file +/livekit-net/ @jhugman +/livekit-token-source/ max.heimbrock@livekit.io From 73e7889c0e77b3be111ccc12737d2d58ed6dbe73 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:01:11 +0200 Subject: [PATCH 18/24] chore: add changeset for livekit-token-source Co-Authored-By: Claude Fable 5 --- .changeset/add_livekit_token_source_crate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add_livekit_token_source_crate.md diff --git a/.changeset/add_livekit_token_source_crate.md b/.changeset/add_livekit_token_source_crate.md new file mode 100644 index 000000000..d0347fc4e --- /dev/null +++ b/.changeset/add_livekit_token_source_crate.md @@ -0,0 +1,5 @@ +--- +livekit-token-source: minor +--- + +Add the `livekit-token-source` crate: token sources for procuring LiveKit credentials, mirroring the JS SDK's `TokenSource` — `literal`, `endpoint` (standard token endpoint format), and `development_token_server`, plus `TokenSourceFixed` / `TokenSourceConfigurable` traits for custom backends. From 9f7aa9c94162c17202b4bcf32fa238da2a27383b Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:40:32 +0200 Subject: [PATCH 19/24] Update livekit-token-source/src/error.rs Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- livekit-token-source/src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/livekit-token-source/src/error.rs b/livekit-token-source/src/error.rs index 0a09834db..dd0230ff4 100644 --- a/livekit-token-source/src/error.rs +++ b/livekit-token-source/src/error.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// Errors returned when procuring credentials from a token source. #[derive(Debug, thiserror::Error)] pub enum TokenSourceError { #[error("no HTTP client available; enable a livekit-net backend feature or call livekit_net::set_http_client")] From 97a829942623ca78e7e5242cb400b071288c9be3 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:47:14 +0200 Subject: [PATCH 20/24] Update .github/CODEOWNERS Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2921eb74d..374d5248f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,4 +9,4 @@ /livekit-data-stream/ @ladvoc @1egoman /livekit-wakeword/ @pham-tuan-binh /livekit-net/ @jhugman -/livekit-token-source/ max.heimbrock@livekit.io +/livekit-token-source/ @MaxHeimbrock From fa1678839c987aaffb023e56b8a3ffde2da8e83a Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:16:21 +0200 Subject: [PATCH 21/24] First batch of comment addressing --- Cargo.lock | 2 + examples/token_source/Cargo.toml | 4 +- examples/token_source/src/main.rs | 56 ++++++++--- examples/token_source/token.txt | 1 + livekit-token-source/src/lib.rs | 4 +- livekit-token-source/src/response.rs | 2 - livekit-token-source/src/token_source.rs | 122 +++++++++++------------ 7 files changed, 110 insertions(+), 81 deletions(-) create mode 100644 examples/token_source/token.txt diff --git a/Cargo.lock b/Cargo.lock index a1c767c97..1642c887f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7482,7 +7482,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "token_source" version = "0.1.0" dependencies = [ + "async-trait", "livekit-token-source", + "serde_json", "tokio", ] diff --git a/examples/token_source/Cargo.toml b/examples/token_source/Cargo.toml index 0c055f92f..48e5cf03b 100644 --- a/examples/token_source/Cargo.toml +++ b/examples/token_source/Cargo.toml @@ -6,4 +6,6 @@ publish = false [dependencies] livekit-token-source = { workspace = true, features = ["rustls-tls-native-roots"] } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } \ No newline at end of file +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +async-trait = "0.1" +serde_json = { workspace = true } \ No newline at end of file diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index 1bbe4265b..25c82a9ea 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -1,15 +1,32 @@ use livekit_token_source::{ - TokenSource, TokenSourceConfigurable, TokenSourceFetchOptions, TokenSourceFixed, - TokenSourceResponse, + TokenSourceConfigurable, TokenSourceFetchOptions, TokenSourceFixed, TokenSourceResponse, TokenSourceResult }; +use async_trait::async_trait; + +/// An example for a custom token source that reads credentials from a JSON file, e.g. +/// `{"server_url": "wss://...", "participant_token": "..."}`. +struct FileTokenSource { + path: std::path::PathBuf, +} + +#[async_trait] +impl TokenSourceFixed for FileTokenSource { + async fn fetch( + &self + ) -> TokenSourceResult { + let contents = std::fs::read_to_string(&self.path).map_err(serde_json::Error::io)?; + let response = serde_json::from_str(&contents)?; + Ok(response) + } +} #[tokio::main] async fn main() { // A literal token source returns a fixed set of pre-provisioned credentials. - let literal = TokenSource::literal(TokenSourceResponse { - server_url: "wss://example.livekit.cloud".to_string(), - participant_token: "".to_string(), - }); + let literal = livekit_token_source::literal( + "wss://example.livekit.cloud", + "" + ); match literal.fetch().await { Ok(response) => println!( "literal: server_url={} participant_token={}", @@ -18,19 +35,29 @@ async fn main() { Err(error) => eprintln!("literal fetch failed: {error}"), } + // A custom token source can procure credentials from anywhere; this one + // reads them from a JSON file next to this example's Cargo.toml. + let file_source = FileTokenSource { + path: concat!(env!("CARGO_MANIFEST_DIR"), "/token.txt").into(), + }; + match file_source.fetch().await { + Ok(response) => println!( + "file: server_url={} participant_token={}", + response.server_url, response.participant_token + ), + Err(error) => eprintln!("file fetch failed: {error}"), + } + // The remaining sources query LiveKit's development token server, which // requires the ID of a sandbox created in your LiveKit Cloud project. - let Ok(sandbox_id) = std::env::var("LIVEKIT_SANDBOX_ID") else { - eprintln!("set LIVEKIT_SANDBOX_ID to run the remaining examples"); - return; - }; + let sandbox_id = "test1-xqsb8v".to_string(); let options = TokenSourceFetchOptions::new() .with_room_name("example-room") .with_participant_identity("example-user"); // Development token server: for prototyping only, NOT for production use. - let development_token_server = TokenSource::development_token_server(sandbox_id.clone()); + let development_token_server = livekit_token_source::development_token_server(sandbox_id.clone()); match development_token_server.fetch(&options).await { Ok(response) => println!( "development token server: server_url={} participant_token={}", @@ -41,10 +68,9 @@ async fn main() { // Endpoint: POSTs the fetch options to a token endpoint using the standard // format; here pointed at the same development token server. - let endpoint = TokenSource::endpoint( - "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", - vec![("X-Sandbox-ID".to_string(), sandbox_id)], - ); + let endpoint = livekit_token_source::endpoint( + "https://cloud-api.livekit.io/api/v2/sandbox/connection-details" + ).with_header("X-Sandbox-ID", sandbox_id); match endpoint.fetch(&options).await { Ok(response) => println!( "endpoint: server_url={} participant_token={}", diff --git a/examples/token_source/token.txt b/examples/token_source/token.txt new file mode 100644 index 000000000..6f16c7520 --- /dev/null +++ b/examples/token_source/token.txt @@ -0,0 +1 @@ +{"server_url": "url from file", "participant_token": "token from file"} diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index c5232c13c..4230deafa 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -28,7 +28,9 @@ pub use error::TokenSourceError; pub use request::TokenSourceFetchOptions; pub use response::TokenSourceResponse; pub use response::TokenSourceResult; -pub use token_source::TokenSource; +pub use token_source::literal; +pub use token_source::development_token_server; +pub use token_source::endpoint; pub use token_source::TokenSourceConfigurable; pub use token_source::TokenSourceDevelopmentTokenServer; pub use token_source::TokenSourceEndpoint; diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs index a93c228af..e39723581 100644 --- a/livekit-token-source/src/response.rs +++ b/livekit-token-source/src/response.rs @@ -20,9 +20,7 @@ use crate::error::TokenSourceError; pub struct TokenSourceResponse { // The documented endpoint contract is snake_case; the camelCase aliases // match the leniency of the JS SDK, which parses via proto3 fromJson. - #[serde(alias = "serverUrl")] pub server_url: String, - #[serde(alias = "participantToken")] pub participant_token: String, } diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index 79be5eccc..ff70a7eda 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashMap; + use crate::error::TokenSourceError; -use crate::request::TokenSourceFetchOptions; -use crate::request::TokenSourceRequest; -use crate::response::TokenSourceResponse; -use crate::response::TokenSourceResult; +use crate::request::{TokenSourceFetchOptions, TokenSourceRequest}; +use crate::response::{TokenSourceResponse, TokenSourceResult}; use async_trait::async_trait; use livekit_net::{Header, HttpClientExt}; @@ -31,6 +31,24 @@ pub trait TokenSourceFixed { async fn fetch(&self) -> TokenSourceResult; } +/// The return type of [`TokenSource::literal`]. +pub struct TokenSourceLiteral { + response: TokenSourceResponse, +} + +#[async_trait] +impl TokenSourceFixed for TokenSourceLiteral { + async fn fetch(&self) -> TokenSourceResult { + Ok(self.response.clone()) + } +} + +/// Creates a token source holding a single, literal set of credentials, +/// returned as-is on every fetch. +pub fn literal(server_url: impl Into, participant_token: impl Into) -> TokenSourceLiteral { + TokenSourceLiteral{response: TokenSourceResponse{server_url: server_url.into(), participant_token: participant_token.into()}} +} + /// A token source that generates credentials from per-call /// [`TokenSourceFetchOptions`] (room name, participant identity, agent /// dispatch, ...). @@ -45,67 +63,10 @@ pub trait TokenSourceConfigurable { ) -> TokenSourceResult; } -/// Factory for the token sources shipped with this crate. Not instantiable; -/// use the associated functions to construct a concrete source. -pub enum TokenSource {} - -impl TokenSource { - /// Creates a token source holding a single, literal set of credentials, - /// returned as-is on every fetch. - pub fn literal(response: TokenSourceResponse) -> TokenSourceLiteral { - TokenSourceLiteral { response } - } - - /// Creates a token source that fetches credentials from the given URL - /// using the standard token endpoint format. - /// - /// The given headers are sent along with every request, e.g. for - /// authentication against the endpoint. - /// - /// See - /// for the endpoint contract. - pub fn endpoint( - endpoint_url: impl Into, - headers: Vec<(String, String)>, - ) -> TokenSourceEndpoint { - TokenSourceEndpoint { endpoint_url: endpoint_url.into(), headers } - } - - /// Creates a token source that queries a LiveKit development token server - /// for credentials, for quick prototyping / getting-started use cases. - /// - /// **This token provider is INSECURE and should NOT be used in - /// production.** - /// - /// See . - pub fn development_token_server( - token_server_id: impl Into, - ) -> TokenSourceDevelopmentTokenServer { - TokenSourceDevelopmentTokenServer { - token_source_endpoint: TokenSource::endpoint( - DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL, - vec![(DEVELOPMENT_TOKEN_SERVER_ID_HEADER.to_string(), token_server_id.into())], - ), - } - } -} - -/// The return type of [`TokenSource::literal`]. -pub struct TokenSourceLiteral { - response: TokenSourceResponse, -} - -#[async_trait] -impl TokenSourceFixed for TokenSourceLiteral { - async fn fetch(&self) -> TokenSourceResult { - Ok(self.response.clone()) - } -} - /// The return type of [`TokenSource::endpoint`]. pub struct TokenSourceEndpoint { endpoint_url: String, - headers: Vec<(String, String)>, + headers: HashMap, } #[async_trait] @@ -142,6 +103,27 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { } } +/// Creates a token source that fetches credentials from the given URL +/// using the standard token endpoint format. +/// +/// The given headers are sent along with every request, e.g. for +/// authentication against the endpoint. +/// +/// See +/// for the endpoint contract. +pub fn endpoint( + endpoint_url: impl Into, +) -> TokenSourceEndpoint { + TokenSourceEndpoint { endpoint_url: endpoint_url.into(), headers: HashMap::new() } +} + +impl TokenSourceEndpoint { + pub fn with_header(mut self, key: impl Into, value: impl Into,) -> Self { + self.headers.insert(key.into(), value.into()); + self + } +} + /// The return type of [`TokenSource::development_token_server`]. pub struct TokenSourceDevelopmentTokenServer { token_source_endpoint: TokenSourceEndpoint, @@ -156,3 +138,19 @@ impl TokenSourceConfigurable for TokenSourceDevelopmentTokenServer { self.token_source_endpoint.fetch(options).await } } + +/// Creates a token source that queries a LiveKit development token server +/// for credentials, for quick prototyping / getting-started use cases. +/// +/// **This token provider is INSECURE and should NOT be used in +/// production.** +/// +/// See . +pub fn development_token_server( + token_server_id: impl Into, +) -> TokenSourceDevelopmentTokenServer { + TokenSourceDevelopmentTokenServer { + token_source_endpoint: endpoint(DEVELOPMENT_TOKEN_SERVER_ENDPOINT_URL) + .with_header(DEVELOPMENT_TOKEN_SERVER_ID_HEADER, token_server_id), + } +} From c02c171b58b63484c40d55ed3ea00de1ef51fb57 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:27:12 +0200 Subject: [PATCH 22/24] More comments addressed --- livekit-token-source/src/lib.rs | 2 +- livekit-token-source/src/request.rs | 10 ++++++-- livekit-token-source/src/token_source.rs | 29 +++++++++++++++++++----- livekit-token-source/tests/mock_http.rs | 15 ++++++------ 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index 4230deafa..1ba15f6fc 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -28,9 +28,9 @@ pub use error::TokenSourceError; pub use request::TokenSourceFetchOptions; pub use response::TokenSourceResponse; pub use response::TokenSourceResult; -pub use token_source::literal; pub use token_source::development_token_server; pub use token_source::endpoint; +pub use token_source::literal; pub use token_source::TokenSourceConfigurable; pub use token_source::TokenSourceDevelopmentTokenServer; pub use token_source::TokenSourceEndpoint; diff --git a/livekit-token-source/src/request.rs b/livekit-token-source/src/request.rs index 6391a2356..ba637914f 100644 --- a/livekit-token-source/src/request.rs +++ b/livekit-token-source/src/request.rs @@ -69,8 +69,13 @@ impl TokenSourceFetchOptions { /// Adds the given attributes to the participant attributes, keeping any set previously. /// A key that was already set is overwritten with its new value. - pub fn with_participant_attributes(mut self, value: HashMap) -> Self { - self.participant_attributes.get_or_insert_with(HashMap::new).extend(value); + pub fn with_participant_attributes( + mut self, + value: impl IntoIterator, impl Into)>, + ) -> Self { + self.participant_attributes + .get_or_insert_with(HashMap::new) + .extend(value.into_iter().map(|(k, v)| (k.into(), v.into()))); self } @@ -123,6 +128,7 @@ pub(crate) struct TokenSourceRequest { room_config: Option, } +/// Non-exhaustive list of room config parameter, the full list is in livekit_room.proto #[derive(serde::Serialize)] struct RoomConfig { agents: Vec, diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index ff70a7eda..6b729eb85 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -45,8 +45,16 @@ impl TokenSourceFixed for TokenSourceLiteral { /// Creates a token source holding a single, literal set of credentials, /// returned as-is on every fetch. -pub fn literal(server_url: impl Into, participant_token: impl Into) -> TokenSourceLiteral { - TokenSourceLiteral{response: TokenSourceResponse{server_url: server_url.into(), participant_token: participant_token.into()}} +pub fn literal( + server_url: impl Into, + participant_token: impl Into, +) -> TokenSourceLiteral { + TokenSourceLiteral { + response: TokenSourceResponse { + server_url: server_url.into(), + participant_token: participant_token.into(), + }, + } } /// A token source that generates credentials from per-call @@ -111,17 +119,26 @@ impl TokenSourceConfigurable for TokenSourceEndpoint { /// /// See /// for the endpoint contract. -pub fn endpoint( - endpoint_url: impl Into, -) -> TokenSourceEndpoint { +pub fn endpoint(endpoint_url: impl Into) -> TokenSourceEndpoint { TokenSourceEndpoint { endpoint_url: endpoint_url.into(), headers: HashMap::new() } } impl TokenSourceEndpoint { - pub fn with_header(mut self, key: impl Into, value: impl Into,) -> Self { + /// Adds a single header to the headers sent with every request, keeping any set previously. + pub fn with_header(mut self, key: impl Into, value: impl Into) -> Self { self.headers.insert(key.into(), value.into()); self } + + /// Adds the given headers to the headers sent with every request, keeping any set previously. + /// A key that was already set is overwritten with its new value. + pub fn with_headers( + mut self, + value: impl IntoIterator, impl Into)>, + ) -> Self { + self.headers.extend(value.into_iter().map(|(k, v)| (k.into(), v.into()))); + self + } } /// The return type of [`TokenSource::development_token_server`]. diff --git a/livekit-token-source/tests/mock_http.rs b/livekit-token-source/tests/mock_http.rs index 08a1b21e1..109446ce8 100644 --- a/livekit-token-source/tests/mock_http.rs +++ b/livekit-token-source/tests/mock_http.rs @@ -21,7 +21,7 @@ use livekit_net::{Header, HttpMethod, HttpResponse, TransportError}; use livekit_token_source::{ - TokenSource, TokenSourceConfigurable, TokenSourceError, TokenSourceFetchOptions, + endpoint, TokenSourceConfigurable, TokenSourceError, TokenSourceFetchOptions, }; use std::collections::HashMap; use std::sync::{Mutex, Once}; @@ -88,8 +88,7 @@ fn header<'a>(headers: &'a [Header], name: &str) -> Option<&'a str> { async fn fetch_posts_json_and_parses_response() { install_mock(); let url = "https://token.test/ok"; - let endpoint = - TokenSource::endpoint(url, vec![("X-Sandbox-ID".to_string(), "sandbox-42".to_string())]); + let endpoint = endpoint(url).with_headers([("X-Sandbox-ID", "sandbox-42")]); let options = TokenSourceFetchOptions::new() .with_room_name("my-room") .with_participant_identity("user-123"); @@ -113,7 +112,7 @@ async fn fetch_posts_json_and_parses_response() { #[tokio::test] async fn non_2xx_maps_to_server_error() { install_mock(); - let endpoint = TokenSource::endpoint("https://token.test/server-error", vec![]); + let endpoint = endpoint("https://token.test/server-error"); let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); match err { @@ -128,7 +127,7 @@ async fn non_2xx_maps_to_server_error() { #[tokio::test] async fn invalid_json_maps_to_json_error() { install_mock(); - let endpoint = TokenSource::endpoint("https://token.test/badjson", vec![]); + let endpoint = endpoint("https://token.test/badjson"); let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); assert!(matches!(err, TokenSourceError::Json(_)), "expected Json error, got {err:?}"); @@ -138,7 +137,7 @@ async fn invalid_json_maps_to_json_error() { async fn agent_options_nest_under_room_config() { install_mock(); let url = "https://token.test/agent"; - let endpoint = TokenSource::endpoint(url, vec![]); + let endpoint = endpoint(url); let options = TokenSourceFetchOptions::new() .with_agent_name("my-agent") .with_agent_metadata("meta") @@ -158,7 +157,7 @@ async fn agent_options_nest_under_room_config() { async fn room_config_is_omitted_without_agent_options() { install_mock(); let url = "https://token.test/no-agent"; - let endpoint = TokenSource::endpoint(url, vec![]); + let endpoint = endpoint(url); let options = TokenSourceFetchOptions::new().with_room_name("plain-room"); endpoint.fetch(&options).await.expect("fetch should succeed"); @@ -171,7 +170,7 @@ async fn room_config_is_omitted_without_agent_options() { #[tokio::test] async fn transport_error_maps_to_transport_variant() { install_mock(); - let endpoint = TokenSource::endpoint("https://token.test/connrefused", vec![]); + let endpoint = endpoint("https://token.test/connrefused"); let err = endpoint.fetch(&TokenSourceFetchOptions::new()).await.unwrap_err(); assert!(matches!(err, TokenSourceError::Transport(_)), "expected Transport error, got {err:?}"); From 25ff41efc470440d518af4693807585f5f592310 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:26:40 +0200 Subject: [PATCH 23/24] fmt --- examples/token_source/src/main.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index 25c82a9ea..70e9fd18a 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -1,7 +1,8 @@ +use async_trait::async_trait; use livekit_token_source::{ - TokenSourceConfigurable, TokenSourceFetchOptions, TokenSourceFixed, TokenSourceResponse, TokenSourceResult + TokenSourceConfigurable, TokenSourceFetchOptions, TokenSourceFixed, TokenSourceResponse, + TokenSourceResult, }; -use async_trait::async_trait; /// An example for a custom token source that reads credentials from a JSON file, e.g. /// `{"server_url": "wss://...", "participant_token": "..."}`. @@ -11,9 +12,7 @@ struct FileTokenSource { #[async_trait] impl TokenSourceFixed for FileTokenSource { - async fn fetch( - &self - ) -> TokenSourceResult { + async fn fetch(&self) -> TokenSourceResult { let contents = std::fs::read_to_string(&self.path).map_err(serde_json::Error::io)?; let response = serde_json::from_str(&contents)?; Ok(response) @@ -23,10 +22,8 @@ impl TokenSourceFixed for FileTokenSource { #[tokio::main] async fn main() { // A literal token source returns a fixed set of pre-provisioned credentials. - let literal = livekit_token_source::literal( - "wss://example.livekit.cloud", - "" - ); + let literal = + livekit_token_source::literal("wss://example.livekit.cloud", ""); match literal.fetch().await { Ok(response) => println!( "literal: server_url={} participant_token={}", @@ -37,9 +34,8 @@ async fn main() { // A custom token source can procure credentials from anywhere; this one // reads them from a JSON file next to this example's Cargo.toml. - let file_source = FileTokenSource { - path: concat!(env!("CARGO_MANIFEST_DIR"), "/token.txt").into(), - }; + let file_source = + FileTokenSource { path: concat!(env!("CARGO_MANIFEST_DIR"), "/token.txt").into() }; match file_source.fetch().await { Ok(response) => println!( "file: server_url={} participant_token={}", @@ -57,7 +53,8 @@ async fn main() { .with_participant_identity("example-user"); // Development token server: for prototyping only, NOT for production use. - let development_token_server = livekit_token_source::development_token_server(sandbox_id.clone()); + let development_token_server = + livekit_token_source::development_token_server(sandbox_id.clone()); match development_token_server.fetch(&options).await { Ok(response) => println!( "development token server: server_url={} participant_token={}", @@ -69,8 +66,9 @@ async fn main() { // Endpoint: POSTs the fetch options to a token endpoint using the standard // format; here pointed at the same development token server. let endpoint = livekit_token_source::endpoint( - "https://cloud-api.livekit.io/api/v2/sandbox/connection-details" - ).with_header("X-Sandbox-ID", sandbox_id); + "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", + ) + .with_header("X-Sandbox-ID", sandbox_id); match endpoint.fetch(&options).await { Ok(response) => println!( "endpoint: server_url={} participant_token={}", From fc4e8ffa53f50503c6df7cf8210f4c4c9fb278dc Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:50:09 +0200 Subject: [PATCH 24/24] docs(token-source): fix stale TokenSource:: factory references, scrub sandbox id These belong to the base token-source work rather than the caching layer: README and doc links updated for the free-function factories, the README example fixed to the current endpoint() signature, a stale camelCase-alias comment removed, and the real sandbox id scrubbed from the example. Co-Authored-By: Claude Fable 5 --- examples/token_source/src/main.rs | 2 +- livekit-token-source/README.md | 12 ++++++------ livekit-token-source/src/lib.rs | 5 +++-- livekit-token-source/src/response.rs | 2 -- livekit-token-source/src/token_source.rs | 6 +++--- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/examples/token_source/src/main.rs b/examples/token_source/src/main.rs index 70e9fd18a..5881386d2 100644 --- a/examples/token_source/src/main.rs +++ b/examples/token_source/src/main.rs @@ -46,7 +46,7 @@ async fn main() { // The remaining sources query LiveKit's development token server, which // requires the ID of a sandbox created in your LiveKit Cloud project. - let sandbox_id = "test1-xqsb8v".to_string(); + let sandbox_id = "your sandbox id".to_string(); let options = TokenSourceFetchOptions::new() .with_room_name("example-room") diff --git a/livekit-token-source/README.md b/livekit-token-source/README.md index d878ddefc..550527206 100644 --- a/livekit-token-source/README.md +++ b/livekit-token-source/README.md @@ -3,12 +3,12 @@ Token sources for the LiveKit Rust SDK. A token source procures the credentials — server URL and participant token — needed to join a LiveKit room. -Three sources ship with the crate, constructed via the `TokenSource` factory functions: +Three sources ship with the crate, constructed via factory functions: -- `TokenSource::literal` — a fixed set of pre-provisioned credentials. -- `TokenSource::endpoint` — fetches credentials from a token endpoint implementing the +- `literal` — a fixed set of pre-provisioned credentials. +- `endpoint` — fetches credentials from a token endpoint implementing the [standard format](https://docs.livekit.io/frontends/build/authentication/endpoint/). -- `TokenSource::development_token_server` — queries a LiveKit +- `development_token_server` — queries a LiveKit [development token server](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) for prototyping. **Not for production use.** @@ -16,9 +16,9 @@ Custom credential backends can implement the `TokenSourceFixed` or `TokenSourceC traits. ```rust -use livekit_token_source::{TokenSource, TokenSourceConfigurable, TokenSourceFetchOptions}; +use livekit_token_source::{TokenSourceConfigurable, TokenSourceFetchOptions}; -let source = TokenSource::endpoint("https://example.com/api/token", vec![]); +let source = livekit_token_source::endpoint("https://example.com/api/token"); let options = TokenSourceFetchOptions::new() .with_room_name("my-room") .with_participant_identity("user-123"); diff --git a/livekit-token-source/src/lib.rs b/livekit-token-source/src/lib.rs index 1ba15f6fc..de72cb06b 100644 --- a/livekit-token-source/src/lib.rs +++ b/livekit-token-source/src/lib.rs @@ -16,8 +16,9 @@ //! //! A token source procures the credentials — server URL and participant //! token — needed to join a LiveKit room. Construct one via the -//! [`TokenSource`] factory functions, or implement [`TokenSourceFixed`] / -//! [`TokenSourceConfigurable`] to plug in a custom credential backend. +//! [`literal`] / [`endpoint`] / [`development_token_server`] factory +//! functions, or implement [`TokenSourceFixed`] / [`TokenSourceConfigurable`] +//! to plug in a custom credential backend. mod error; mod request; diff --git a/livekit-token-source/src/response.rs b/livekit-token-source/src/response.rs index e39723581..c576f1c32 100644 --- a/livekit-token-source/src/response.rs +++ b/livekit-token-source/src/response.rs @@ -18,8 +18,6 @@ use crate::error::TokenSourceError; /// the participant token to authenticate with. #[derive(Debug, Clone, serde::Deserialize)] pub struct TokenSourceResponse { - // The documented endpoint contract is snake_case; the camelCase aliases - // match the leniency of the JS SDK, which parses via proto3 fromJson. pub server_url: String, pub participant_token: String, } diff --git a/livekit-token-source/src/token_source.rs b/livekit-token-source/src/token_source.rs index 6b729eb85..56293cb2d 100644 --- a/livekit-token-source/src/token_source.rs +++ b/livekit-token-source/src/token_source.rs @@ -31,7 +31,7 @@ pub trait TokenSourceFixed { async fn fetch(&self) -> TokenSourceResult; } -/// The return type of [`TokenSource::literal`]. +/// The return type of [`literal`]. pub struct TokenSourceLiteral { response: TokenSourceResponse, } @@ -71,7 +71,7 @@ pub trait TokenSourceConfigurable { ) -> TokenSourceResult; } -/// The return type of [`TokenSource::endpoint`]. +/// The return type of [`endpoint`]. pub struct TokenSourceEndpoint { endpoint_url: String, headers: HashMap, @@ -141,7 +141,7 @@ impl TokenSourceEndpoint { } } -/// The return type of [`TokenSource::development_token_server`]. +/// The return type of [`development_token_server`]. pub struct TokenSourceDevelopmentTokenServer { token_source_endpoint: TokenSourceEndpoint, }