Skip to content

Conversation

1egoman
Copy link

@1egoman 1egoman commented Oct 15, 2025

Talking with @ladvoc, it seemed like it would be a good exercise to port the TokenSource to rust as a first pass to see what new additional complexity is exposed in a less client focused context. Plus longer term, this could be useful as part of the broader rust client sdk core project.

For a working example, see examples/token_source. You'll want to run it with some environment variables populated - something maybe like this would work: LIVEKIT_URL=1 LIVEKIT_API_KEY=2 LIVEKIT_API_SECRET=3 SERVER_URL=4 API_KEY=5 API_SECRET=6 RUST_LOG=info cargo run

Warning

General caveat: I've yet to write a substantial amount of rust professionally, though personally I've written a fair bit. Because of this it's fairly likely I'm doing some things badly / in non idiomatic ways. Feel free to point any of that kind of stuff out!

High level summary

The literal, endpoint, sandboxTokenServer, and custom implementations all work very similar to their web, swift, and android counterparts.

The big new concept this change introduces is the TokenSourceMinter / TokenSourceMinterCustom. Roughly what they look like:

let fetch_options = TokenSourceFetchOptions::default().with_agent_name("voice ai quickstart");

// A TokenSourceMinter is a configurable token source that lets a user "mint" new tokens with a given set of options:
let minter = TokenSourceMinter::default();
let _ = minter_literal_custom.fetch(&fetch_options).await;

// There's also a mechanism to allow users to pass different sets of credentials to the minter either from literal
// strings (below), environment variables (the default), or via a manual closure evaluated at fetch time.
let minter_literal = TokenSourceMinter::new(MinterCredentials::new("server url", "api key", "api secret"));
let _ = minter_literal_credentials.fetch(&fetch_options).await;

// One other type of TokenSource I added - I'm not sure if this one is useful / a good idea though...
//
// A TokenSourceCustomMinter is a fixed TokenSource which calls its provided closure with a preconfigured
// access token builder which allows you to manually update and return a new generated token. It seems like
// this might be a better approach for one off use cases while a `TokenSourceMinter` would be better for use
// with a future agents sdk which could pass in an agent name / etc.
let custom_minter =
    TokenSourceCustomMinter::new(|access_token| {
        access_token
            .with_identity("rust-bot")
            .with_name("Rust Bot")
            .with_grants(access_token::VideoGrants {
                room_join: true,
                room: "my-room".to_string(),
                ..Default::default()
            })
            .to_jwt()
    });
let _ = custom_minter.fetch().await;

Large remaining things to work through

  • Add some sort of caching / jwt validation logic to TokenSourceConfigurable or maybe a TokenSourceCached composition layer like swift/android or something like that
  • Get async closures working within TokenSourceCustom
  • Get async closures working within TokenSourceLiteral
  • Error handling
  • Tests

Comment on lines 13 to 82
/// There is also a predefined list of labels that can be used to reference common metrics.
/// They have reserved indices from 0 to (METRIC_LABEL_PREDEFINED_MAX_VALUE - 1).
/// Indexes pointing at str_data should start from METRIC_LABEL_PREDEFINED_MAX_VALUE,
/// Indexes pointing at str_data should start from METRIC_LABEL_PREDEFINED_MAX_VALUE,
/// such that str_data\[0\] == index of METRIC_LABEL_PREDEFINED_MAX_VALUE.
#[prost(string, repeated, tag="3")]
pub str_data: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(message, repeated, tag="4")]
pub time_series: ::prost::alloc::vec::Vec<TimeSeriesMetric>,
#[prost(message, repeated, tag="5")]
pub events: ::prost::alloc::vec::Vec<EventMetric>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TimeSeriesMetric {
/// Metric name e.g "speech_probablity". The string value is not directly stored in the message, but referenced by index
/// in the `str_data` field of `MetricsBatch`
#[prost(uint32, tag="1")]
pub label: u32,
/// index into `str_data`
#[prost(uint32, tag="2")]
pub participant_identity: u32,
/// index into `str_data`
#[prost(uint32, tag="3")]
pub track_sid: u32,
#[prost(message, repeated, tag="4")]
pub samples: ::prost::alloc::vec::Vec<MetricSample>,
/// index into 'str_data'
#[prost(uint32, tag="5")]
pub rid: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MetricSample {
/// time of metric based on a monotonic clock (in milliseconds)
#[prost(int64, tag="1")]
pub timestamp_ms: i64,
#[prost(message, optional, tag="2")]
pub normalized_timestamp: ::core::option::Option<::pbjson_types::Timestamp>,
#[prost(float, tag="3")]
pub value: f32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct EventMetric {
#[prost(uint32, tag="1")]
pub label: u32,
/// index into `str_data`
#[prost(uint32, tag="2")]
pub participant_identity: u32,
/// index into `str_data`
#[prost(uint32, tag="3")]
pub track_sid: u32,
/// start time of event based on a monotonic clock (in milliseconds)
#[prost(int64, tag="4")]
pub start_timestamp_ms: i64,
/// end time of event based on a monotonic clock (in milliseconds), if needed
#[prost(int64, optional, tag="5")]
pub end_timestamp_ms: ::core::option::Option<i64>,
#[prost(message, optional, tag="6")]
pub normalized_start_timestamp: ::core::option::Option<::pbjson_types::Timestamp>,
#[prost(message, optional, tag="7")]
pub normalized_end_timestamp: ::core::option::Option<::pbjson_types::Timestamp>,
#[prost(string, tag="8")]
pub metadata: ::prost::alloc::string::String,
/// index into 'str_data'
#[prost(uint32, tag="9")]
pub rid: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MetricsRecordingHeader {
#[prost(string, tag="1")]
pub room_id: ::prost::alloc::string::String,
#[prost(bool, optional, tag="2")]
pub enable_user_data_training: ::core::option::Option<bool>,
}
//
// Protocol used to record metrics for a specific session.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to update the livekit-protocol package to the latest in order to get access to TokenSourceRequest / TokenSourceResponse from here - that's where the majority of the diff changes are coming from:

Image

Is this something worth doing as a separate pull request merged in as a prerequisite or is it fine to include this broader change in here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is we are using an old version of prost, and when you install the generator locally, you get the latest by default. Options:

  1. Install the correct generator: cargo install [email protected] [email protected]
  2. Take this PR out of draft, and the CI will generate for you using the correct version

I'm going to create a ticket to remind us we need to upgrade to prost 14.x.x.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed the instructions in here so I think I did generate these with the correct version, FYI. That being said I also added a note for myself to add some better documentation (maybe add it to the README.md?) on how to generate these, it took me longer than it should have for me to self-discover how.

Comment on lines 45 to 49
Sip,
Agent,
Connector,
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along with the livekit-protocol changes, I had to add a new corresponding Connector value to this enum to get it to compile again. I don't know what this pertains to / why this was added, so I wanted to call it out just to make sure somebody else more familiar with the state of this repository can verify I did the right thing here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, yea, I didn't do something right here and this is what is causing the ci build to break... 😞

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see the issue. Since you added the Connector variant in the public ParticipantKind enum, you are getting a build error because this isn't being exposed over FFI. Steps to fix:

  1. Add the variant in the FFI protocol in livekit-ffi/protocol/participant.proto
  2. Add a match arm in livekit-ffi/src/conversion/participant.rs (this is the source of the build error)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also curious what a connector participant is

Copy link
Author

@1egoman 1egoman Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see - I didn't realize there were separate FFI protobuf implementations which is what was tripping me up. Just made that change and was able to do a build of livekit-ffi locally, thanks!

Copy link
Author

@1egoman 1egoman Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like CI is now passing everywhere except for android for an unknown reason. Any thoughts on why? It doesn't at least at surface level seem to be related to my change but I'm not 100% sure.

Failing android build logs:

error: failed to run custom build command for `openssl-sys v0.9.109`

Caused by:
  process didn't exit successfully: `CARGO=/home/runner/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo CARGO_CFG_FEATURE='' CARGO_CFG_PANIC=unwind CARGO_CFG_TARGET_ABI='' CARGO_CFG_TARGET_ARCH=aarch64 CARGO_CFG_TARGET_ENDIAN=little CARGO_CFG_TARGET_ENV='' CARGO_CFG_TARGET_FAMILY=unix CARGO_CFG_TARGET_FEATURE=neon CARGO_CFG_TARGET_HAS_ATOMIC=128,16,32,64,8,ptr CARGO_CFG_TARGET_OS=android CARGO_CFG_TARGET_POINTER_WIDTH=64 CARGO_CFG_TARGET_VENDOR=unknown CARGO_CFG_UNIX='' CARGO_ENCODED_RUSTFLAGS='' CARGO_MANIFEST_DIR=/home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openssl-sys-0.9.109 CARGO_MANIFEST_LINKS=openssl CARGO_MANIFEST_PATH=/home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openssl-sys-0.9.109/Cargo.toml CARGO_PKG_AUTHORS='Alex Crichton <[email protected]>:Steven Fackler <[email protected]>' CARGO_PKG_DESCRIPTION='FFI bindings to OpenSSL' CARGO_PKG_HOMEPAGE='' CARGO_PKG_LICENSE=MIT CARGO_PKG_LICENSE_FILE='' CARGO_PKG_NAME=openssl-sys CARGO_PKG_README=README.md CARGO_PKG_REPOSITORY='https://github.com/sfackler/rust-openssl' CARGO_PKG_RUST_VERSION=1.63.0 CARGO_PKG_VERSION=0.9.109 CARGO_PKG_VERSION_MAJOR=0 CARGO_PKG_VERSION_MINOR=9 CARGO_PKG_VERSION_PATCH=109 CARGO_PKG_VERSION_PRE='' DEBUG=false HOST=x86_64-unknown-linux-gnu LD_LIBRARY_PATH='/home/runner/work/rust-sdks/rust-sdks/target/release/deps:/home/runner/work/rust-sdks/rust-sdks/target/release:/home/runner/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib:/home/runner/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib' NUM_JOBS=4 OPT_LEVEL=3 OUT_DIR=/home/runner/work/rust-sdks/rust-sdks/target/aarch64-linux-android/release/build/openssl-sys-855bcbdb9481fece/out PROFILE=release RUSTC=/home/runner/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/rustc RUSTC_LINKER=/home/runner/.cargo/bin/cargo-ndk RUSTDOC=/home/runner/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/rustdoc TARGET=aarch64-linux-android /home/runner/work/rust-sdks/rust-sdks/target/release/build/openssl-sys-9005c0badb4c225b/build-script-main` (exit status: 101)
  --- stdout
  cargo:rustc-check-cfg=cfg(osslconf, values("OPENSSL_NO_OCB", "OPENSSL_NO_SM4", "OPENSSL_NO_SEED", "OPENSSL_NO_CHACHA", "OPENSSL_NO_CAST", "OPENSSL_NO_IDEA", "OPENSSL_NO_CAMELLIA", "OPENSSL_NO_RC4", "OPENSSL_NO_BF", "OPENSSL_NO_PSK", "OPENSSL_NO_DEPRECATED_3_0", "OPENSSL_NO_SCRYPT", "OPENSSL_NO_SM3", "OPENSSL_NO_RMD160", "OPENSSL_NO_EC2M", "OPENSSL_NO_OCSP", "OPENSSL_NO_CMS", "OPENSSL_NO_COMP", "OPENSSL_NO_SOCK", "OPENSSL_NO_STDIO", "OPENSSL_NO_EC", "OPENSSL_NO_SSL3_METHOD", "OPENSSL_NO_KRB5", "OPENSSL_NO_TLSEXT", "OPENSSL_NO_SRP", "OPENSSL_NO_RFC3779", "OPENSSL_NO_SHA", "OPENSSL_NO_NEXTPROTONEG", "OPENSSL_NO_ENGINE", "OPENSSL_NO_BUF_FREELISTS", "OPENSSL_NO_RC2"))
  cargo:rustc-check-cfg=cfg(openssl)
  cargo:rustc-check-cfg=cfg(libressl)
  cargo:rustc-check-cfg=cfg(boringssl)
  cargo:rustc-check-cfg=cfg(awslc)
  cargo:rustc-check-cfg=cfg(libressl250)
  cargo:rustc-check-cfg=cfg(libressl251)
  cargo:rustc-check-cfg=cfg(libressl252)
  cargo:rustc-check-cfg=cfg(libressl261)
  cargo:rustc-check-cfg=cfg(libressl270)
  cargo:rustc-check-cfg=cfg(libressl271)
  cargo:rustc-check-cfg=cfg(libressl273)
  cargo:rustc-check-cfg=cfg(libressl280)
  cargo:rustc-check-cfg=cfg(libressl281)
  cargo:rustc-check-cfg=cfg(libressl291)
  cargo:rustc-check-cfg=cfg(libressl310)
  cargo:rustc-check-cfg=cfg(libressl321)
  cargo:rustc-check-cfg=cfg(libressl332)
  cargo:rustc-check-cfg=cfg(libressl340)
  cargo:rustc-check-cfg=cfg(libressl350)
  cargo:rustc-check-cfg=cfg(libressl360)
  cargo:rustc-check-cfg=cfg(libressl361)
  cargo:rustc-check-cfg=cfg(libressl370)
  cargo:rustc-check-cfg=cfg(libressl380)
  cargo:rustc-check-cfg=cfg(libressl381)
  cargo:rustc-check-cfg=cfg(libressl382)
  cargo:rustc-check-cfg=cfg(libressl390)
  cargo:rustc-check-cfg=cfg(libressl400)
  cargo:rustc-check-cfg=cfg(libressl410)
  cargo:rustc-check-cfg=cfg(ossl101)
  cargo:rustc-check-cfg=cfg(ossl102)
  cargo:rustc-check-cfg=cfg(ossl102f)
  cargo:rustc-check-cfg=cfg(ossl102h)
  cargo:rustc-check-cfg=cfg(ossl110)
  cargo:rustc-check-cfg=cfg(ossl110f)
  cargo:rustc-check-cfg=cfg(ossl110g)
  cargo:rustc-check-cfg=cfg(ossl110h)
  cargo:rustc-check-cfg=cfg(ossl111)
  cargo:rustc-check-cfg=cfg(ossl111b)
  cargo:rustc-check-cfg=cfg(ossl111c)
  cargo:rustc-check-cfg=cfg(ossl111d)
  cargo:rustc-check-cfg=cfg(ossl300)
  cargo:rustc-check-cfg=cfg(ossl310)
  cargo:rustc-check-cfg=cfg(ossl320)
  cargo:rustc-check-cfg=cfg(ossl330)
  cargo:rustc-check-cfg=cfg(ossl340)
  cargo:rerun-if-env-changed=AARCH64_LINUX_ANDROID_OPENSSL_LIB_DIR
  AARCH64_LINUX_ANDROID_OPENSSL_LIB_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_LIB_DIR
  OPENSSL_LIB_DIR unset
  cargo:rerun-if-env-changed=AARCH64_LINUX_ANDROID_OPENSSL_INCLUDE_DIR
  AARCH64_LINUX_ANDROID_OPENSSL_INCLUDE_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_INCLUDE_DIR
  OPENSSL_INCLUDE_DIR unset
  cargo:rerun-if-env-changed=AARCH64_LINUX_ANDROID_OPENSSL_DIR
  AARCH64_LINUX_ANDROID_OPENSSL_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_DIR
  OPENSSL_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_NO_PKG_CONFIG
  cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64-linux-android
  cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS_aarch64_linux_android
  cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_ALLOW_CROSS
  cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS
  cargo:rerun-if-env-changed=PKG_CONFIG_aarch64-linux-android
  cargo:rerun-if-env-changed=PKG_CONFIG_aarch64_linux_android
  cargo:rerun-if-env-changed=TARGET_PKG_CONFIG
  cargo:rerun-if-env-changed=PKG_CONFIG
  cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64-linux-android
  cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_aarch64_linux_android
  cargo:rerun-if-env-changed=TARGET_PKG_CONFIG_SYSROOT_DIR
  cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR


  Could not find openssl via pkg-config:
  pkg-config has not been configured to support cross-compilation.

  Install a sysroot for the target platform and configure it via
  PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH, or install a
  cross-compiling wrapper for pkg-config and set it via
  PKG_CONFIG environment variable.

  cargo:warning=Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge. If OpenSSL is installed and this crate had trouble finding it,  you can set the `OPENSSL_DIR` environment variable for the compilation process. See stderr section below for further information.

  --- stderr


  Could not find directory of OpenSSL installation, and this `-sys` crate cannot
  proceed without this knowledge. If OpenSSL is installed and this crate had
  trouble finding it,  you can set the `OPENSSL_DIR` environment variable for the
  compilation process.

  Make sure you also have the development packages of openssl installed.
  For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora.

  If you're in a situation where you think the directory *should* be found
  automatically, please open a bug at https://github.com/sfackler/rust-openssl
  and include information about your system as well as this message.

  $HOST = x86_64-unknown-linux-gnu
  $TARGET = aarch64-linux-android
  openssl-sys = 0.9.109

Comment on lines +49 to +56

impl<F: Fn() -> TokenSourceResponse> TokenLiteralGenerator for F {
// FIXME: allow this to be an async fn!
fn apply(&self) -> TokenSourceResponse {
self()
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: this needs to be changed to accept an async function, so you can do something like:

let _ = TokenSourceLiteral::new(async || {
  Ok(TokenSourceResponse { /* ... */})
});

Comment on lines +88 to +100

// FIXME: apply options in the below code!
let participant_token = access_token::AccessToken::with_api_key(&api_key, &api_secret)
.with_identity("rust-bot")
.with_name("Rust Bot")
.with_grants(access_token::VideoGrants {
room_join: true,
room: "my-room".to_string(),
..Default::default()
})
.to_jwt()?;

Ok(TokenSourceResponse { server_url, participant_token })
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: finish this up by wiring all the options values through to the corresponding with_ prefixed builder functions!

Comment on lines 220 to 249
pub struct TokenSourceCustom<
CustomFn: Fn(
&TokenSourceFetchOptions,
) -> Pin<Box<dyn Future<Output = Result<TokenSourceResponse, Box<dyn Error>>>>>,
>(CustomFn);

impl<
CustomFn: Fn(
&TokenSourceFetchOptions,
) -> Pin<Box<dyn Future<Output = Result<TokenSourceResponse, Box<dyn Error>>>>>,
> TokenSourceCustom<CustomFn>
{
pub fn new(custom_fn: CustomFn) -> Self {
Self(custom_fn)
}
}

impl<
CustomFn: Fn(
&TokenSourceFetchOptions,
) -> Pin<Box<dyn Future<Output = Result<TokenSourceResponse, Box<dyn Error>>>>>,
> TokenSourceConfigurable for TokenSourceCustom<CustomFn>
{
async fn fetch(
&self,
options: &TokenSourceFetchOptions,
) -> Result<TokenSourceResponse, Box<dyn Error>> {
(self.0)(options).await
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TokenSourceCustom is a bit of a mess and still isn't really doing what I want. Ideally, I'd like to be able to pass an async |options| {} as CustomFn but I can't quite get that to work.

The closest I've been able to get is:

let custom = TokenSourceCustom::new(|_options| {
    Box::pin(future::ready(Ok(TokenSourceResponse {
        server_url: "...".into(),
        participant_token: "... _options should be encoded in here ...".into(),
    })))
});

Maybe somebody else has some ideas?

Comment on lines 32 to 36
/// A helper trait to more easily implement a TokenSourceFixed which not async.
pub trait TokenSourceFixedSynchronous {
// FIXME: what should the error type of the result be?
fn fetch_synchronous(&self) -> Result<TokenSourceResponse, Box<dyn Error>>;
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to call out: I added -Synchronous versions of the token source traits to try to cut down on the boilerplate in non async downstream implementors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: What do you think about abbreviating Synchronous to Sync?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had them ending in Sync, but my concern was that it would imply some sort of relationship with the Sync trait. It sounds like you don't think that's a concern though?


// FIXME: What are the best practices around implementing Into on a reference to avoid the
// clone?
let request_body: TokenSourceRequest = options.clone().into();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought:

  • You currently implement impl Into<TokenSourceRequest> for TokenSourceFetchOptions
  • Try implementing impl Into<TokenSourceRequest> for &TokenSourceFetchOptions instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! I think what I was hoping for was some way to transparently handle both impl Into<TokenSourceRequest> for TokenSourceFetchOptions and impl Into<TokenSourceRequest> for &TokenSourceFetchOptions through some sort of implicit dereferencing or something like that, but it might just not be possible.

That being said, now that I'm thinking about it more - because TokenSourceRequest contains Strings, if I'm converting a & TokenSourceFetchOptions into a TokenSourceRequest, there's going to have to be some sort of cloning involved at some level unless I did something more crazy with a Cow / etc in TokenSourceRequest.

Because of this, I'm not sure where that clone should happen, but I tend to think forcing the user to do it makes it clear that it has to happen verses it happening implicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants