/*
* Copyright 2026 LiveKit
*
* 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.
*/
#include "livekit/token_source.h"
#include
#include
#include
#include
#include
#include "token_source_internal.h"
namespace livekit {
namespace {
using TokenSourceResult = Result;
using TokenSourceFuture = std::future;
constexpr const char* kDefaultSandboxBaseUrl = "https://cloud-api.livekit.io";
bool tokenRequestOptionsEqual(const TokenRequestOptions& a, const TokenRequestOptions& b) {
return a.room_name == b.room_name && a.participant_name == b.participant_name &&
a.participant_identity == b.participant_identity && a.participant_metadata == b.participant_metadata &&
a.participant_attributes == b.participant_attributes && a.agent_name == b.agent_name &&
a.agent_metadata == b.agent_metadata && a.agent_deployment == b.agent_deployment;
}
TokenSourceFuture makeFailedFuture(std::string message) {
std::promise promise;
promise.set_value(TokenSourceResult::failure(TokenSourceError{std::move(message)}));
return promise.get_future();
}
template
TokenSourceFuture runAsyncTokenSource(std::string context, WorkFn&& work_fn) {
try {
return std::async(std::launch::async,
[context = std::move(context), work_fn = std::forward(work_fn)]() mutable {
try {
return work_fn();
} catch (const std::exception& e) {
return TokenSourceResult::failure(TokenSourceError{context + ": " + std::string(e.what())});
} catch (...) {
return TokenSourceResult::failure(TokenSourceError{context + ": unknown exception"});
}
});
} catch (const std::exception& e) {
return makeFailedFuture(context + ": failed to start async work: " + std::string(e.what()));
} catch (...) {
return makeFailedFuture(context + ": failed to start async work: unknown exception");
}
}
std::string trimSandboxId(const std::string& sandbox_id) {
const auto is_space = [](unsigned char ch) { return std::isspace(ch) != 0; };
const auto begin = std::find_if_not(sandbox_id.begin(), sandbox_id.end(), is_space);
const auto end = std::find_if_not(sandbox_id.rbegin(), sandbox_id.rend(), is_space).base();
if (begin >= end) {
return {};
}
return std::string(begin, end);
}
std::string joinUrlPath(const std::string& base_url, const std::string& path) {
if (base_url.empty()) {
return path;
}
if (base_url.back() == '/') {
if (path.empty()) {
return base_url;
}
if (path.front() == '/') {
return base_url + path.substr(1);
}
return base_url + path;
}
if (path.empty()) {
return base_url;
}
if (path.front() == '/') {
return base_url + path;
}
return base_url + "/" + path;
}
struct ResolvedSandboxEndpoint {
std::string url;
TokenEndpointOptions options;
};
// Apply the sandbox header and resolve the connection-details URL shared by the
// production and test-only sandbox factories.
ResolvedSandboxEndpoint resolveSandboxEndpoint(const std::string& sandbox_id, TokenEndpointOptions options,
const std::string& base_url) {
options.headers["X-Sandbox-ID"] = trimSandboxId(sandbox_id);
const std::string resolved_base_url = base_url.empty() ? kDefaultSandboxBaseUrl : base_url;
return {joinUrlPath(resolved_base_url, "/api/v2/sandbox/connection-details"), std::move(options)};
}
} // namespace
std::unique_ptr LiteralTokenSource::create(std::string server_url, std::string participant_token) {
TokenSourceResponse details;
details.server_url = std::move(server_url);
details.participant_token = std::move(participant_token);
return std::unique_ptr(new LiteralTokenSource(std::move(details)));
}
std::unique_ptr LiteralTokenSource::create(
std::function provider) {
return std::unique_ptr(new LiteralTokenSource(std::move(provider)));
}
LiteralTokenSource::LiteralTokenSource(TokenSourceResponse details) : details_(std::move(details)) {}
LiteralTokenSource::LiteralTokenSource(
std::function provider)
: provider_(std::move(provider)) {}
std::future LiteralTokenSource::fetch() {
if (provider_) {
return provider_();
}
return std::async(std::launch::deferred, [details = details_]() {
if (details.server_url.empty() || details.participant_token.empty()) {
return Result::failure(
TokenSourceError{"literal token source returned empty server_url or participant_token"});
}
return Result::success(details);
});
}
std::unique_ptr CustomTokenSource::create(
std::function provider) {
return std::unique_ptr(new CustomTokenSource(std::move(provider)));
}
CustomTokenSource::CustomTokenSource(
std::function provider)
: provider_(std::move(provider)) {}
std::future CustomTokenSource::fetch(
const TokenRequestOptions& options) {
return provider_(options);
}
std::unique_ptr EndpointTokenSource::create(std::string endpoint_url,
TokenEndpointOptions options) {
return std::unique_ptr(
new EndpointTokenSource(std::move(endpoint_url), std::move(options), &tokenSourceHttpRequest));
}
EndpointTokenSource::EndpointTokenSource(std::string endpoint_url, TokenEndpointOptions options,
HttpTransport transport)
: endpoint_url_(std::move(endpoint_url)), options_(std::move(options)), transport_(std::move(transport)) {}
std::unique_ptr EndpointTokenSourceTestAccess::create(std::string endpoint_url,
TokenEndpointOptions options,
TokenSourceHttpTransport transport) {
return std::unique_ptr(
new EndpointTokenSource(std::move(endpoint_url), std::move(options), std::move(transport)));
}
std::future EndpointTokenSource::fetch(
const TokenRequestOptions& options) {
std::shared_ptr options_snapshot;
try {
options_snapshot = std::make_shared(options);
} catch (const std::exception& e) {
return makeFailedFuture("token source endpoint fetch failed: failed to copy request options: " +
std::string(e.what()));
} catch (...) {
return makeFailedFuture("token source endpoint fetch failed: failed to copy request options: unknown exception");
}
return runAsyncTokenSource("token source endpoint fetch failed",
[this, options_snapshot]() { return fetchSync(*options_snapshot); });
}
Result EndpointTokenSource::fetchSync(const TokenRequestOptions& options) const {
const std::string request_json = buildTokenSourceRequestJson(options);
auto headers = options_.headers;
auto http_result = transport_(options_.method, endpoint_url_, headers, request_json, options_.timeout);
if (!http_result) {
return Result::failure(
TokenSourceError{"token server request failed: " + http_result.error()});
}
return parseTokenSourceResponseJson(http_result.value());
}
std::unique_ptr SandboxTokenSource::create(const std::string& sandbox_id,
const SandboxTokenServerOptions& options) {
auto resolved = resolveSandboxEndpoint(sandbox_id, {}, options.base_url);
auto endpoint = EndpointTokenSource::create(std::move(resolved.url), std::move(resolved.options));
return std::unique_ptr(new SandboxTokenSource(std::move(endpoint)));
}
SandboxTokenSource::SandboxTokenSource(std::unique_ptr endpoint)
: endpoint_(std::move(endpoint)) {}
std::unique_ptr SandboxTokenSourceTestAccess::create(const std::string& sandbox_id,
const SandboxTokenServerOptions& options,
TokenSourceHttpTransport transport) {
auto resolved = resolveSandboxEndpoint(sandbox_id, {}, options.base_url);
auto endpoint =
EndpointTokenSourceTestAccess::create(std::move(resolved.url), std::move(resolved.options), std::move(transport));
return std::unique_ptr(new SandboxTokenSource(std::move(endpoint)));
}
std::future SandboxTokenSource::fetch(
const TokenRequestOptions& options) {
return endpoint_->fetch(options);
}
std::unique_ptr CachingTokenSource::create(std::unique_ptr inner) {
return std::unique_ptr(new CachingTokenSource(std::move(inner)));
}
CachingTokenSource::CachingTokenSource(std::unique_ptr inner) : inner_(std::move(inner)) {}
std::future CachingTokenSource::fetch(
const TokenRequestOptions& options) {
std::shared_ptr options_snapshot;
try {
options_snapshot = std::make_shared(options);
} catch (const std::exception& e) {
return makeFailedFuture("token source cache fetch failed: failed to copy request options: " +
std::string(e.what()));
} catch (...) {
return makeFailedFuture("token source cache fetch failed: failed to copy request options: unknown exception");
}
return runAsyncTokenSource("token source cache fetch failed", [this, options_snapshot]() {
const std::scoped_lock lock(mutex_);
if (cached_details_.has_value() && cached_options_.has_value() &&
tokenRequestOptionsEqual(*cached_options_, *options_snapshot) &&
isParticipantTokenValid(cached_details_->participant_token)) {
return TokenSourceResult::success(*cached_details_);
}
auto result = inner_->fetch(*options_snapshot).get();
if (result) {
cached_options_ = *options_snapshot;
cached_details_ = result.value();
}
return result;
});
}
void CachingTokenSource::invalidate() {
const std::scoped_lock lock(mutex_);
cached_options_.reset();
cached_details_.reset();
}
std::optional CachingTokenSource::cachedResponse() const {
const std::scoped_lock lock(mutex_);
return cached_details_;
}
} // namespace livekit