From 416a48eb73946366f1c0136d42886d4c14721b72 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:36:44 +0200 Subject: [PATCH 01/12] Switch from sandbox to develop wording in code --- Editor/TokenSourceComponentConfigEditor.cs | 8 ++++---- Runtime/Scripts/TokenSource/TokenSource.cs | 13 ++++++++++--- .../Scripts/TokenSource/TokenSourceComponent.cs | 6 +++--- .../TokenSource/TokenSourceComponentConfig.cs | 14 +++++++------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Editor/TokenSourceComponentConfigEditor.cs b/Editor/TokenSourceComponentConfigEditor.cs index 03650d98..ed234714 100644 --- a/Editor/TokenSourceComponentConfigEditor.cs +++ b/Editor/TokenSourceComponentConfigEditor.cs @@ -25,12 +25,12 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty("_token")); break; - case TokenSourceType.Sandbox: + case TokenSourceType.Develop: EditorGUILayout.HelpBox( - "Use this for development to create tokens from a sandbox token server. " + - "\nWARNING: ONLY USE THIS OPTION FOR LOCAL DEVELOPMENT, SINCE THE SANDBOX TOKEN SERVER NEEDS NO AUTHENTICATION.", + "Use this for development to create tokens from a development token server. " + + "\nWARNING: ONLY USE THIS OPTION FOR LOCAL DEVELOPMENT, SINCE THE DEVELOPMENT TOKEN SERVER NEEDS NO AUTHENTICATION.", MessageType.Info); - EditorGUILayout.PropertyField(serializedObject.FindProperty("_sandboxId")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("_tokenServerId")); DrawConnectionOptions(); break; diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index 9c9ba09e..a83d3b76 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -184,13 +184,20 @@ private static string NullIfEmpty(string value) => string.IsNullOrEmpty(value) ? null : value; } + + [Obsolete("Use TokenSourceDevelop instead")] + public class TokenSourceSandbox : TokenSourceDevelop + { + public TokenSourceSandbox(string sandboxId) : base(sandboxId) {} + } + /// - /// Convenience preconfigured for LiveKit Cloud sandbox token servers. + /// Convenience preconfigured for LiveKit Cloud development token servers. /// Intended for development and testing only — see /// https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/. /// - public class TokenSourceSandbox : TokenSourceEndpoint + public class TokenSourceDevelop : TokenSourceEndpoint { - public TokenSourceSandbox(string sandboxId) : base("https://cloud-api.livekit.io/api/v2/sandbox/connection-details", new[] { new StringPair { key = "X-Sandbox-ID", value = sandboxId } }) {} + public TokenSourceDevelop(string tokenServerId) : base("https://cloud-api.livekit.io/api/v2/sandbox/connection-details", new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }) {} } } \ No newline at end of file diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs index ebd104d6..b64342bc 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs @@ -10,7 +10,7 @@ namespace LiveKit /// /// MonoBehaviour wrapper that builds an from an inspector-assigned /// ScriptableObject. To skip the asset entirely, instantiate - /// , , , + /// , , , /// or directly at runtime. /// public class TokenSourceComponent : MonoBehaviour @@ -34,8 +34,8 @@ public void Awake() switch (_config.TokenSourceType) { - case TokenSourceType.Sandbox: - _tokenSource = new TokenSourceSandbox(_config.SandboxId); + case TokenSourceType.Develop: + _tokenSource = new TokenSourceDevelop(_config.TokenServerId); break; case TokenSourceType.Endpoint: diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs index 28f20a5d..3a3054e0 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs @@ -7,7 +7,7 @@ namespace LiveKit public enum TokenSourceType { Literal, - Sandbox, + Develop, Endpoint } @@ -27,14 +27,14 @@ public class TokenSourceComponentConfig : ScriptableObject [SerializeField] private string _serverUrl; [SerializeField] private string _token; - // Sandbox fields - [SerializeField] private string _sandboxId; + // Develop fields + [SerializeField] private string _tokenServerId; // Endpoint fields [SerializeField] private string _endpointUrl; [SerializeField] private List _endpointHeaders; - // Shared connection options (Sandbox + Endpoint) + // Shared connection options (Develop + Endpoint) [SerializeField] private string _roomName; [SerializeField] private string _participantName; [SerializeField] private string _participantIdentity; @@ -50,8 +50,8 @@ public class TokenSourceComponentConfig : ScriptableObject public string ServerUrl => _serverUrl; public string Token => _token; - // Sandbox - public string SandboxId => _sandboxId?.Trim('"'); + // Develop + public string TokenServerId => _tokenServerId?.Trim('"'); // Endpoint public string EndpointUrl => _endpointUrl; @@ -70,7 +70,7 @@ public class TokenSourceComponentConfig : ScriptableObject public bool IsValid => _tokenSourceType switch { TokenSourceType.Literal => !string.IsNullOrEmpty(ServerUrl) && ServerUrl.StartsWith("ws") && !string.IsNullOrEmpty(Token), - TokenSourceType.Sandbox => !string.IsNullOrEmpty(SandboxId), + TokenSourceType.Develop => !string.IsNullOrEmpty(TokenServerId), TokenSourceType.Endpoint => !string.IsNullOrEmpty(EndpointUrl), _ => false }; From 1c15ebe878f9b9775cf81fb51e8f06edad380aeb Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:44:53 +0200 Subject: [PATCH 02/12] Rename sandbox to development token server in READMEs Co-Authored-By: Claude Fable 5 --- README.md | 8 ++++---- Samples~/Agents/README.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 81af3049..1a57c6c2 100644 --- a/README.md +++ b/README.md @@ -137,11 +137,11 @@ To help getting started with tokens, use `TokenSourceComponent.cs` with a `Token #### 1. Literal Use this to pass a pregenerated server URL and token. Generate tokens via the [LiveKit CLI](https://docs.livekit.io/frontends/build/authentication/custom/#manual-token-creation) or from your [LiveKit Cloud](https://cloud.livekit.io/) project's API key page. -#### 2. Sandbox -For development and testing. Follow the [sandbox token server guide](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) to enable your project's sandbox and get the sandbox ID. Optional connection fields (room name, participant name, agent name, etc.) can be configured in the inspector — leave blank for server defaults. +#### 2. Develop +For development and testing. Follow the [development token server guide](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) to enable your project's development token server and get the token server ID. Optional connection fields (room name, participant name, agent name, etc.) can be configured in the inspector — leave blank for server defaults. #### 3. Endpoint -For production. Point to your own token endpoint URL and add any required authentication headers. Uses the same connection options as Sandbox. See the [endpoint token generation guide](https://docs.livekit.io/frontends/build/authentication/endpoint/). +For production. Point to your own token endpoint URL and add any required authentication headers. Uses the same connection options as Develop. See the [endpoint token generation guide](https://docs.livekit.io/frontends/build/authentication/endpoint/). #### Usage @@ -186,7 +186,7 @@ yield return fetch; var details = fetch.Result; // Configurable sources accept TokenSourceFetchOptions per call: -ITokenSourceConfigurable configurable = new TokenSourceSandbox(""); +ITokenSourceConfigurable configurable = new TokenSourceDevelop(""); // or: new TokenSourceEndpoint("https://your.token-server/api/token", headers); var configurableFetch = configurable.FetchConnectionDetails(new TokenSourceFetchOptions { RoomName = "lobby" }); diff --git a/Samples~/Agents/README.md b/Samples~/Agents/README.md index a7cf76eb..c7ab9bbe 100644 --- a/Samples~/Agents/README.md +++ b/Samples~/Agents/README.md @@ -20,9 +20,9 @@ The app is configured to connect to the LiveKit homepage agent by default, which To switch from the default agent to your own, you first need a LiveKit agent to speak with. For a no-code setup, use the [Agent Builder](https://docs.livekit.io/agents/start/builder/). For more customization, try our starter agent for [Python](https://github.com/livekit-examples/agent-starter-python), [Node.js](https://github.com/livekit-examples/agent-starter-node), or [create your own from scratch](https://docs.livekit.io/agents/start/voice-ai/). -Second, you need a token server. For development, the easiest option is the sandbox token server: enable it from your project's Options on the Settings page in LiveKit Cloud and copy the sandboxId. +Second, you need a token server. For development, the easiest option is the development token server: enable it from your project's Options on the Settings page in LiveKit Cloud and copy the token server ID. -Then create a new TokenSoureComponentConfig asset for your sandbox and reference it in the scene on the `TokenSourceComponent` script instead of the `HomepageAgent.asset`: +Then create a new TokenSoureComponentConfig asset for your development token server and reference it in the scene on the `TokenSourceComponent` script instead of the `HomepageAgent.asset`: ### Common sample package From da06aa74d7a3dce8cf02692c80b0ba88bccb1e8e Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:02:59 +0200 Subject: [PATCH 03/12] Rename Develop to Development for token source naming Co-Authored-By: Claude Fable 5 --- Editor/TokenSourceComponentConfigEditor.cs | 2 +- README.md | 6 +++--- Runtime/Scripts/TokenSource/TokenSource.cs | 8 ++++---- Runtime/Scripts/TokenSource/TokenSourceComponent.cs | 6 +++--- .../Scripts/TokenSource/TokenSourceComponentConfig.cs | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Editor/TokenSourceComponentConfigEditor.cs b/Editor/TokenSourceComponentConfigEditor.cs index ed234714..581af79c 100644 --- a/Editor/TokenSourceComponentConfigEditor.cs +++ b/Editor/TokenSourceComponentConfigEditor.cs @@ -25,7 +25,7 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty("_token")); break; - case TokenSourceType.Develop: + case TokenSourceType.Development: EditorGUILayout.HelpBox( "Use this for development to create tokens from a development token server. " + "\nWARNING: ONLY USE THIS OPTION FOR LOCAL DEVELOPMENT, SINCE THE DEVELOPMENT TOKEN SERVER NEEDS NO AUTHENTICATION.", diff --git a/README.md b/README.md index 1a57c6c2..005cf88c 100644 --- a/README.md +++ b/README.md @@ -137,11 +137,11 @@ To help getting started with tokens, use `TokenSourceComponent.cs` with a `Token #### 1. Literal Use this to pass a pregenerated server URL and token. Generate tokens via the [LiveKit CLI](https://docs.livekit.io/frontends/build/authentication/custom/#manual-token-creation) or from your [LiveKit Cloud](https://cloud.livekit.io/) project's API key page. -#### 2. Develop +#### 2. Development For development and testing. Follow the [development token server guide](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) to enable your project's development token server and get the token server ID. Optional connection fields (room name, participant name, agent name, etc.) can be configured in the inspector — leave blank for server defaults. #### 3. Endpoint -For production. Point to your own token endpoint URL and add any required authentication headers. Uses the same connection options as Develop. See the [endpoint token generation guide](https://docs.livekit.io/frontends/build/authentication/endpoint/). +For production. Point to your own token endpoint URL and add any required authentication headers. Uses the same connection options as Development. See the [endpoint token generation guide](https://docs.livekit.io/frontends/build/authentication/endpoint/). #### Usage @@ -186,7 +186,7 @@ yield return fetch; var details = fetch.Result; // Configurable sources accept TokenSourceFetchOptions per call: -ITokenSourceConfigurable configurable = new TokenSourceDevelop(""); +ITokenSourceConfigurable configurable = new TokenSourceDevelopment(""); // or: new TokenSourceEndpoint("https://your.token-server/api/token", headers); var configurableFetch = configurable.FetchConnectionDetails(new TokenSourceFetchOptions { RoomName = "lobby" }); diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index a83d3b76..91ab2e56 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -185,8 +185,8 @@ private static string NullIfEmpty(string value) => } - [Obsolete("Use TokenSourceDevelop instead")] - public class TokenSourceSandbox : TokenSourceDevelop + [Obsolete("Use TokenSourceDevelopment instead")] + public class TokenSourceSandbox : TokenSourceDevelopment { public TokenSourceSandbox(string sandboxId) : base(sandboxId) {} } @@ -196,8 +196,8 @@ public TokenSourceSandbox(string sandboxId) : base(sandboxId) {} /// Intended for development and testing only — see /// https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/. /// - public class TokenSourceDevelop : TokenSourceEndpoint + public class TokenSourceDevelopment : TokenSourceEndpoint { - public TokenSourceDevelop(string tokenServerId) : base("https://cloud-api.livekit.io/api/v2/sandbox/connection-details", new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }) {} + public TokenSourceDevelopment(string tokenServerId) : base("https://cloud-api.livekit.io/api/v2/sandbox/connection-details", new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }) {} } } \ No newline at end of file diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs index b64342bc..31f630db 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs @@ -10,7 +10,7 @@ namespace LiveKit /// /// MonoBehaviour wrapper that builds an from an inspector-assigned /// ScriptableObject. To skip the asset entirely, instantiate - /// , , , + /// , , , /// or directly at runtime. /// public class TokenSourceComponent : MonoBehaviour @@ -34,8 +34,8 @@ public void Awake() switch (_config.TokenSourceType) { - case TokenSourceType.Develop: - _tokenSource = new TokenSourceDevelop(_config.TokenServerId); + case TokenSourceType.Development: + _tokenSource = new TokenSourceDevelopment(_config.TokenServerId); break; case TokenSourceType.Endpoint: diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs index 3a3054e0..89f2d199 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs @@ -7,7 +7,7 @@ namespace LiveKit public enum TokenSourceType { Literal, - Develop, + Development, Endpoint } @@ -27,14 +27,14 @@ public class TokenSourceComponentConfig : ScriptableObject [SerializeField] private string _serverUrl; [SerializeField] private string _token; - // Develop fields + // Development fields [SerializeField] private string _tokenServerId; // Endpoint fields [SerializeField] private string _endpointUrl; [SerializeField] private List _endpointHeaders; - // Shared connection options (Develop + Endpoint) + // Shared connection options (Development + Endpoint) [SerializeField] private string _roomName; [SerializeField] private string _participantName; [SerializeField] private string _participantIdentity; @@ -50,7 +50,7 @@ public class TokenSourceComponentConfig : ScriptableObject public string ServerUrl => _serverUrl; public string Token => _token; - // Develop + // Development public string TokenServerId => _tokenServerId?.Trim('"'); // Endpoint @@ -70,7 +70,7 @@ public class TokenSourceComponentConfig : ScriptableObject public bool IsValid => _tokenSourceType switch { TokenSourceType.Literal => !string.IsNullOrEmpty(ServerUrl) && ServerUrl.StartsWith("ws") && !string.IsNullOrEmpty(Token), - TokenSourceType.Develop => !string.IsNullOrEmpty(TokenServerId), + TokenSourceType.Development => !string.IsNullOrEmpty(TokenServerId), TokenSourceType.Endpoint => !string.IsNullOrEmpty(EndpointUrl), _ => false }; From f03bcd8451a1fdc7e00a5b03d5d55be5b4f2fc4e Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:24:34 +0200 Subject: [PATCH 04/12] Formerly serialized as to make alias for old assets work --- Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs index 89f2d199..896e1030 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using UnityEngine; +using UnityEngine.Serialization; namespace LiveKit { @@ -28,6 +29,7 @@ public class TokenSourceComponentConfig : ScriptableObject [SerializeField] private string _token; // Development fields + [FormerlySerializedAs("_sandboxId")] [SerializeField] private string _tokenServerId; // Endpoint fields From 9a9c46138612c542a3ef30e771b0c5e7e12278b5 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:38:24 +0200 Subject: [PATCH 05/12] Hide token source implementations behind the TokenSource factory The concrete implementations are now private sealed classes nested in the static TokenSource factory, so construction is only possible through the factory methods, which return ITokenSourceFixed or ITokenSourceConfigurable. The development token server is a preconfigured endpoint source; the deprecated SandboxTokenServer method forwards to it. Co-Authored-By: Claude Fable 5 --- Runtime/Scripts/TokenSource/TokenSource.cs | 300 ++++++++++-------- .../TokenSource/TokenSourceComponent.cs | 18 +- .../TokenSource/TokenSourceComponentConfig.cs | 4 +- 3 files changed, 174 insertions(+), 148 deletions(-) diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index 91ab2e56..0fc325db 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -9,195 +9,219 @@ namespace LiveKit { /// - /// Marker interface for any source of LiveKit . - /// Implementations are either or . - /// - public interface ITokenSource - { - } - - /// - /// A token source whose connection details are fully determined at construction time and cannot be - /// influenced by per-call options (e.g. literal credentials or a user-supplied callback). - /// - public interface ITokenSourceFixed : ITokenSource - { - public TaskYieldInstruction FetchConnectionDetails(); - } - - /// - /// A token source that accepts per-call to parameterize the - /// request (e.g. an HTTP endpoint that needs room/participant info per fetch). - /// - public interface ITokenSourceConfigurable : ITokenSource - { - public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options); - } - - /// - /// Returns a fixed server URL and participant token. Suitable when credentials are pregenerated - /// (e.g. via the LiveKit CLI or LiveKit Cloud project page). + /// Factory for the built-in implementations. The concrete implementations + /// are private; obtain them through the factory methods and work with the returned + /// or . /// - public class TokenSourceLiteral : ITokenSourceFixed + public static class TokenSource { - private string _serverUrl; - private string _participantToken; + public delegate Task CustomTokenFunction(); - public TokenSourceLiteral(string serverUrl, string participantToken) + /// + /// Returns a fixed server URL and participant token. Suitable when credentials are pregenerated + /// (e.g. via the LiveKit CLI or LiveKit Cloud project page). + /// + public static ITokenSourceFixed Literal(string serverUrl, string participantToken) { - _serverUrl = serverUrl; - _participantToken = participantToken; + return new LiteralSource(serverUrl, participantToken); } - public TaskYieldInstruction FetchConnectionDetails() + /// + /// Posts a JSON request to a token-server endpoint and returns the parsed . + /// The body is built from per-call (room name, participant info, + /// agent dispatch, etc.). Use for production token servers — see + /// https://docs.livekit.io/frontends/build/authentication/endpoint/. + /// + public static ITokenSourceConfigurable Endpoint(string endpointUrl, IEnumerable headers) { - var result = new ConnectionDetails { ServerUrl = _serverUrl, ParticipantToken = _participantToken }; - return new TaskYieldInstruction(Task.FromResult(result)); + return new EndpointSource(endpointUrl, headers); } - } - /// - /// Delegates connection-detail retrieval to a user-supplied async function. Use this when your - /// app already has its own token-fetching code (custom auth flow, cached tokens, etc.). - /// - public class TokenSourceCustom : ITokenSourceFixed - { - public delegate Task CustomTokenFunction(); + /// + /// Delegates connection-detail retrieval to a user-supplied async function. Use this when your + /// app already has its own token-fetching code (custom auth flow, cached tokens, etc.). + /// + public static ITokenSourceFixed Custom(CustomTokenFunction customTokenFunction) + { + return new CustomSource(customTokenFunction); + } - private CustomTokenFunction _customTokenFunction; + [Obsolete("Use TokenSource.DevelopmentTokenServer instead")] + public static ITokenSourceConfigurable SandboxTokenServer(string sandboxId) + { + return DevelopmentTokenServer(sandboxId); + } - public TokenSourceCustom(CustomTokenFunction customTokenFunction) + /// + /// Convenience preconfigured for LiveKit Cloud development token servers. + /// Intended for development and testing only — see + /// https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/. + /// + public static ITokenSourceConfigurable DevelopmentTokenServer(string tokenServerId) { - _customTokenFunction = customTokenFunction; + return new EndpointSource( + "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", + new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }); } - public TaskYieldInstruction FetchConnectionDetails() + private sealed class LiteralSource : ITokenSourceFixed { - // Route a synchronous throw (or a null return) from the user's function through the - // instruction's IsError/Exception, so callers never have to guard the call itself. - Task task; - try + private string _serverUrl; + private string _participantToken; + + public LiteralSource(string serverUrl, string participantToken) { - task = _customTokenFunction() - ?? Task.FromException( - new InvalidOperationException("Custom token function returned a null task")); + _serverUrl = serverUrl; + _participantToken = participantToken; } - catch (Exception e) + + public TaskYieldInstruction FetchConnectionDetails() { - task = Task.FromException(e); + var result = new ConnectionDetails { ServerUrl = _serverUrl, ParticipantToken = _participantToken }; + return new TaskYieldInstruction(Task.FromResult(result)); } - return new TaskYieldInstruction(task); - } - } - - /// - /// Posts a JSON request to a token-server endpoint and returns the parsed . - /// The body is built from per-call (room name, participant info, - /// agent dispatch, etc.). Use for production token servers — see - /// https://docs.livekit.io/frontends/build/authentication/endpoint/. - /// - public class TokenSourceEndpoint : ITokenSourceConfigurable - { - private string _endpointUrl; - IEnumerable _headers; - private static readonly HttpClient HttpClient = new HttpClient(); - - public TokenSourceEndpoint(string endpointUrl, IEnumerable headers) - { - _endpointUrl = endpointUrl; - _headers = headers; } - public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) + private sealed class CustomSource : ITokenSourceFixed { - // Async methods can't return the (non-awaitable) instruction directly, so the actual - // request lives in the helper below; the returned task carries any synchronous throw. - return new TaskYieldInstruction(FetchConnectionDetailsAsync(options)); - } + private CustomTokenFunction _customTokenFunction; - private async Task FetchConnectionDetailsAsync(TokenSourceFetchOptions options) - { - var requestBody = BuildRequest(options); - var jsonBody = JsonConvert.SerializeObject(requestBody); + public CustomSource(CustomTokenFunction customTokenFunction) + { + _customTokenFunction = customTokenFunction; + } - var request = new HttpRequestMessage(HttpMethod.Post, _endpointUrl); - if (_headers != null) + public TaskYieldInstruction FetchConnectionDetails() { - foreach (var header in _headers) + // Route a synchronous throw (or a null return) from the user's function through the + // instruction's IsError/Exception, so callers never have to guard the call itself. + Task task; + try + { + task = _customTokenFunction() + ?? Task.FromException( + new InvalidOperationException("Custom token function returned a null task")); + } + catch (Exception e) { - if (!string.IsNullOrEmpty(header.key)) - request.Headers.TryAddWithoutValidation(header.key, header.value); + task = Task.FromException(e); } + return new TaskYieldInstruction(task); } - var content = new StringContent(jsonBody, System.Text.Encoding.UTF8); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - request.Content = content; - - var response = await HttpClient.SendAsync(request); - - if (!response.IsSuccessStatusCode) - throw new InvalidOperationException($"Token server error: {response.StatusCode}, response: {await response.Content.ReadAsStringAsync()}"); - - var jsonContent = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(jsonContent); } - private static TokenSourceRequest BuildRequest(TokenSourceFetchOptions options) + private sealed class EndpointSource : ITokenSourceConfigurable { - var request = new TokenSourceRequest + private string _endpointUrl; + IEnumerable _headers; + private static readonly HttpClient HttpClient = new HttpClient(); + + public EndpointSource(string endpointUrl, IEnumerable headers) { - RoomName = NullIfEmpty(options.RoomName), - ParticipantName = NullIfEmpty(options.ParticipantName), - ParticipantIdentity = NullIfEmpty(options.ParticipantIdentity), - ParticipantMetadata = NullIfEmpty(options.ParticipantMetadata), - }; + _endpointUrl = endpointUrl; + _headers = headers; + } - if (options.ParticipantAttributes != null && options.ParticipantAttributes.Count > 0) + public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) { - request.ParticipantAttributes = options.ParticipantAttributes - .Where(a => !string.IsNullOrEmpty(a.Key)) - .ToDictionary(a => a.Key, a => a.Value); - if (request.ParticipantAttributes.Count == 0) - request.ParticipantAttributes = null; + // Async methods can't return the (non-awaitable) instruction directly, so the actual + // request lives in the helper below; the returned task carries any synchronous throw. + return new TaskYieldInstruction(FetchConnectionDetailsAsync(options)); } - if (!string.IsNullOrEmpty(options.AgentName) || !string.IsNullOrEmpty(options.AgentMetadata) || !string.IsNullOrEmpty(options.AgentDeployment)) + private async Task FetchConnectionDetailsAsync(TokenSourceFetchOptions options) { - request.RoomConfig = new RoomConfig + var requestBody = BuildRequest(options); + var jsonBody = JsonConvert.SerializeObject(requestBody); + + var request = new HttpRequestMessage(HttpMethod.Post, _endpointUrl); + if (_headers != null) { - Agents = new List + foreach (var header in _headers) { - new AgentDispatch - { - AgentName = NullIfEmpty(options.AgentName), - Metadata = NullIfEmpty(options.AgentMetadata), - Deployment = NullIfEmpty(options.AgentDeployment) - } + if (!string.IsNullOrEmpty(header.key)) + request.Headers.TryAddWithoutValidation(header.key, header.value); } + } + var content = new StringContent(jsonBody, System.Text.Encoding.UTF8); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + request.Content = content; + + var response = await HttpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + throw new InvalidOperationException($"Token server error: {response.StatusCode}, response: {await response.Content.ReadAsStringAsync()}"); + + var jsonContent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(jsonContent); + } + + private static TokenSourceRequest BuildRequest(TokenSourceFetchOptions options) + { + var request = new TokenSourceRequest + { + RoomName = NullIfEmpty(options.RoomName), + ParticipantName = NullIfEmpty(options.ParticipantName), + ParticipantIdentity = NullIfEmpty(options.ParticipantIdentity), + ParticipantMetadata = NullIfEmpty(options.ParticipantMetadata), }; + + if (options.ParticipantAttributes != null && options.ParticipantAttributes.Count > 0) + { + request.ParticipantAttributes = options.ParticipantAttributes + .Where(a => !string.IsNullOrEmpty(a.Key)) + .ToDictionary(a => a.Key, a => a.Value); + if (request.ParticipantAttributes.Count == 0) + request.ParticipantAttributes = null; + } + + if (!string.IsNullOrEmpty(options.AgentName) || !string.IsNullOrEmpty(options.AgentMetadata) || !string.IsNullOrEmpty(options.AgentDeployment)) + { + request.RoomConfig = new RoomConfig + { + Agents = new List + { + new AgentDispatch + { + AgentName = NullIfEmpty(options.AgentName), + Metadata = NullIfEmpty(options.AgentMetadata), + Deployment = NullIfEmpty(options.AgentDeployment) + } + } + }; + } + + return request; } - return request; + private static string NullIfEmpty(string value) => + string.IsNullOrEmpty(value) ? null : value; } + } - private static string NullIfEmpty(string value) => - string.IsNullOrEmpty(value) ? null : value; + /// + /// Marker interface for any source of LiveKit . + /// Implementations are either or . + /// + public interface ITokenSource + { } - - [Obsolete("Use TokenSourceDevelopment instead")] - public class TokenSourceSandbox : TokenSourceDevelopment + /// + /// A token source whose connection details are fully determined at construction time and cannot be + /// influenced by per-call options (e.g. literal credentials or a user-supplied callback). + /// + public interface ITokenSourceFixed : ITokenSource { - public TokenSourceSandbox(string sandboxId) : base(sandboxId) {} + public TaskYieldInstruction FetchConnectionDetails(); } /// - /// Convenience preconfigured for LiveKit Cloud development token servers. - /// Intended for development and testing only — see - /// https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/. + /// A token source that accepts per-call to parameterize the + /// request (e.g. an HTTP endpoint that needs room/participant info per fetch). /// - public class TokenSourceDevelopment : TokenSourceEndpoint + public interface ITokenSourceConfigurable : ITokenSource { - public TokenSourceDevelopment(string tokenServerId) : base("https://cloud-api.livekit.io/api/v2/sandbox/connection-details", new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }) {} + public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options); } -} \ No newline at end of file +} diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs index 31f630db..2d80d23a 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponent.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponent.cs @@ -9,9 +9,10 @@ namespace LiveKit { /// /// MonoBehaviour wrapper that builds an from an inspector-assigned - /// ScriptableObject. To skip the asset entirely, instantiate - /// , , , - /// or directly at runtime. + /// ScriptableObject. To skip the asset entirely, create a + /// source at runtime via the factory methods (, + /// , , + /// or ). /// public class TokenSourceComponent : MonoBehaviour { @@ -34,16 +35,16 @@ public void Awake() switch (_config.TokenSourceType) { - case TokenSourceType.Development: - _tokenSource = new TokenSourceDevelopment(_config.TokenServerId); + case TokenSourceType.DevelopmentTokenServer: + _tokenSource = TokenSource.DevelopmentTokenServer(_config.TokenServerId); break; case TokenSourceType.Endpoint: - _tokenSource = new TokenSourceEndpoint(_config.EndpointUrl, _config.EndpointHeaders); + _tokenSource = TokenSource.Endpoint(_config.EndpointUrl, _config.EndpointHeaders); break; case TokenSourceType.Literal: - _tokenSource = new TokenSourceLiteral(_config.ServerUrl, _config.Token); + _tokenSource = TokenSource.Literal(_config.ServerUrl, _config.Token); break; default: @@ -55,7 +56,8 @@ public void Awake() /// Fetches connection details, merging per-call over the asset-backed /// . For each field, a value provided on /// overrides the config value (empty strings are treated as unset and fall through to the config). - /// Ignored for fixed token sources (, ). + /// Ignored for fixed token sources (, i.e. those created via + /// or ). /// public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) { diff --git a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs index 896e1030..0ea4c166 100644 --- a/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs +++ b/Runtime/Scripts/TokenSource/TokenSourceComponentConfig.cs @@ -8,7 +8,7 @@ namespace LiveKit public enum TokenSourceType { Literal, - Development, + DevelopmentTokenServer, Endpoint } @@ -72,7 +72,7 @@ public class TokenSourceComponentConfig : ScriptableObject public bool IsValid => _tokenSourceType switch { TokenSourceType.Literal => !string.IsNullOrEmpty(ServerUrl) && ServerUrl.StartsWith("ws") && !string.IsNullOrEmpty(Token), - TokenSourceType.Development => !string.IsNullOrEmpty(TokenServerId), + TokenSourceType.DevelopmentTokenServer => !string.IsNullOrEmpty(TokenServerId), TokenSourceType.Endpoint => !string.IsNullOrEmpty(EndpointUrl), _ => false }; From a3788c924fd838a31e2a691da4265fd5d5a168ea Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:40:04 +0200 Subject: [PATCH 06/12] Update README token source usage to factory methods Co-Authored-By: Claude Fable 5 --- README.md | 10 +++++----- Samples~/Agents/README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 005cf88c..75a3c189 100644 --- a/README.md +++ b/README.md @@ -174,20 +174,20 @@ var fetch = _tokenSourceComponent.FetchConnectionDetails(new TokenSourceFetchOpt }); ``` -To skip the ScriptableObject entirely, instantiate a token source directly. Each returns the same `TaskYieldInstruction` from `FetchConnectionDetails`, so it can be yielded, awaited, or `.AsUniTask()`-bridged just like the component: +To skip the ScriptableObject entirely, create a token source at runtime via the `TokenSource` factory methods. Each returns the same `TaskYieldInstruction` from `FetchConnectionDetails`, so it can be yielded, awaited, or `.AsUniTask()`-bridged just like the component: ```cs // Fixed sources take no per-call options: -ITokenSourceFixed source = new TokenSourceLiteral("wss://your.livekit.host", ""); -// or: new TokenSourceCustom(async () => await MyAuthFlow()); +ITokenSourceFixed source = TokenSource.Literal("wss://your.livekit.host", ""); +// or: TokenSource.Custom(async () => await MyAuthFlow()); var fetch = source.FetchConnectionDetails(); yield return fetch; var details = fetch.Result; // Configurable sources accept TokenSourceFetchOptions per call: -ITokenSourceConfigurable configurable = new TokenSourceDevelopment(""); -// or: new TokenSourceEndpoint("https://your.token-server/api/token", headers); +ITokenSourceConfigurable configurable = TokenSource.DevelopmentTokenServer(""); +// or: TokenSource.Endpoint("https://your.token-server/api/token", headers); var configurableFetch = configurable.FetchConnectionDetails(new TokenSourceFetchOptions { RoomName = "lobby" }); yield return configurableFetch; diff --git a/Samples~/Agents/README.md b/Samples~/Agents/README.md index c7ab9bbe..6ee57f2d 100644 --- a/Samples~/Agents/README.md +++ b/Samples~/Agents/README.md @@ -22,7 +22,7 @@ To switch from the default agent to your own, you first need a LiveKit agent to Second, you need a token server. For development, the easiest option is the development token server: enable it from your project's Options on the Settings page in LiveKit Cloud and copy the token server ID. -Then create a new TokenSoureComponentConfig asset for your development token server and reference it in the scene on the `TokenSourceComponent` script instead of the `HomepageAgent.asset`: +Then create a new TokenSourceComponentConfig asset for your development token server and reference it in the scene on the `TokenSourceComponent` script instead of the `HomepageAgent.asset`: ### Common sample package From 48d67207782965e3a211145b52525ae02a7041e1 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:50:08 +0200 Subject: [PATCH 07/12] Editor script needed update as well --- Editor/TokenSourceComponentConfigEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/TokenSourceComponentConfigEditor.cs b/Editor/TokenSourceComponentConfigEditor.cs index 581af79c..fba7ff16 100644 --- a/Editor/TokenSourceComponentConfigEditor.cs +++ b/Editor/TokenSourceComponentConfigEditor.cs @@ -25,7 +25,7 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty("_token")); break; - case TokenSourceType.Development: + case TokenSourceType.DevelopmentTokenServer: EditorGUILayout.HelpBox( "Use this for development to create tokens from a development token server. " + "\nWARNING: ONLY USE THIS OPTION FOR LOCAL DEVELOPMENT, SINCE THE DEVELOPMENT TOKEN SERVER NEEDS NO AUTHENTICATION.", From c8805c3120aa0d23ccf23368d564dad7ac6bcc88 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:07:09 +0200 Subject: [PATCH 08/12] Restore v2.0.0 token source classes as obsolete shims TokenSourceLiteral, TokenSourceCustom, TokenSourceEndpoint and TokenSourceSandbox remain publicly constructible but are marked [Obsolete], pointing to the TokenSource factory methods. They are thin wrappers delegating to the private implementations, matching the constructor signatures shipped in v2.0.0 (including the nested CustomTokenFunction delegate and the TokenSourceSandbox inheritance from TokenSourceEndpoint). Co-Authored-By: Claude Fable 5 --- Runtime/Scripts/TokenSource/TokenSource.cs | 56 +++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index 0fc325db..8545e769 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -17,6 +17,8 @@ public static class TokenSource { public delegate Task CustomTokenFunction(); + internal const string DevelopmentTokenServerUrl = "https://cloud-api.livekit.io/api/v2/sandbox/connection-details"; + /// /// Returns a fixed server URL and participant token. Suitable when credentials are pregenerated /// (e.g. via the LiveKit CLI or LiveKit Cloud project page). @@ -60,7 +62,7 @@ public static ITokenSourceConfigurable SandboxTokenServer(string sandboxId) public static ITokenSourceConfigurable DevelopmentTokenServer(string tokenServerId) { return new EndpointSource( - "https://cloud-api.livekit.io/api/v2/sandbox/connection-details", + DevelopmentTokenServerUrl, new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }); } @@ -224,4 +226,56 @@ public interface ITokenSourceConfigurable : ITokenSource { public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options); } + + [Obsolete("Use TokenSource.Literal(...) instead")] + public class TokenSourceLiteral : ITokenSourceFixed + { + private readonly ITokenSourceFixed _inner; + + public TokenSourceLiteral(string serverUrl, string participantToken) + { + _inner = TokenSource.Literal(serverUrl, participantToken); + } + + public TaskYieldInstruction FetchConnectionDetails() => _inner.FetchConnectionDetails(); + } + + [Obsolete("Use TokenSource.Custom(...) instead")] + public class TokenSourceCustom : ITokenSourceFixed + { + // v2.0.0 declared the delegate nested here; keep it so explicit + // TokenSourceCustom.CustomTokenFunction references still compile. + public delegate Task CustomTokenFunction(); + + private readonly ITokenSourceFixed _inner; + + public TokenSourceCustom(CustomTokenFunction customTokenFunction) + { + // Lambda (not .Invoke) so a null delegate surfaces at fetch time via + // IsError/Exception, matching v2.0.0 behavior, not as a ctor throw. + _inner = TokenSource.Custom(() => customTokenFunction()); + } + + public TaskYieldInstruction FetchConnectionDetails() => _inner.FetchConnectionDetails(); + } + + [Obsolete("Use TokenSource.Endpoint(...) instead")] + public class TokenSourceEndpoint : ITokenSourceConfigurable + { + private readonly ITokenSourceConfigurable _inner; + + public TokenSourceEndpoint(string endpointUrl, IEnumerable headers) + { + _inner = TokenSource.Endpoint(endpointUrl, headers); + } + + public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) => _inner.FetchConnectionDetails(options); + } + + [Obsolete("Use TokenSource.DevelopmentTokenServer(...) instead")] + public class TokenSourceSandbox : TokenSourceEndpoint + { + public TokenSourceSandbox(string sandboxId) + : base(TokenSource.DevelopmentTokenServerUrl, new[] { new StringPair { key = "X-Sandbox-ID", value = sandboxId } }) {} + } } From c89d26579fb0fb17e2714011148954b5d84bf9fa Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:09:32 +0200 Subject: [PATCH 09/12] Align nested private classes to same naming as before --- Runtime/Scripts/TokenSource/TokenSource.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index 8545e769..681426d5 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -25,7 +25,7 @@ public static class TokenSource /// public static ITokenSourceFixed Literal(string serverUrl, string participantToken) { - return new LiteralSource(serverUrl, participantToken); + return new TokenSourceLiteral(serverUrl, participantToken); } /// @@ -36,7 +36,7 @@ public static ITokenSourceFixed Literal(string serverUrl, string participantToke /// public static ITokenSourceConfigurable Endpoint(string endpointUrl, IEnumerable headers) { - return new EndpointSource(endpointUrl, headers); + return new TokenSourceEndpoint(endpointUrl, headers); } /// @@ -45,7 +45,7 @@ public static ITokenSourceConfigurable Endpoint(string endpointUrl, IEnumerable< /// public static ITokenSourceFixed Custom(CustomTokenFunction customTokenFunction) { - return new CustomSource(customTokenFunction); + return new TokenSourceCustom(customTokenFunction); } [Obsolete("Use TokenSource.DevelopmentTokenServer instead")] @@ -61,17 +61,17 @@ public static ITokenSourceConfigurable SandboxTokenServer(string sandboxId) /// public static ITokenSourceConfigurable DevelopmentTokenServer(string tokenServerId) { - return new EndpointSource( + return new TokenSourceEndpoint( DevelopmentTokenServerUrl, new[] { new StringPair { key = "X-Sandbox-ID", value = tokenServerId } }); } - private sealed class LiteralSource : ITokenSourceFixed + private sealed class TokenSourceLiteral : ITokenSourceFixed { private string _serverUrl; private string _participantToken; - public LiteralSource(string serverUrl, string participantToken) + public TokenSourceLiteral(string serverUrl, string participantToken) { _serverUrl = serverUrl; _participantToken = participantToken; @@ -84,11 +84,11 @@ public TaskYieldInstruction FetchConnectionDetails() } } - private sealed class CustomSource : ITokenSourceFixed + private sealed class TokenSourceCustom : ITokenSourceFixed { private CustomTokenFunction _customTokenFunction; - public CustomSource(CustomTokenFunction customTokenFunction) + public TokenSourceCustom(CustomTokenFunction customTokenFunction) { _customTokenFunction = customTokenFunction; } @@ -112,13 +112,13 @@ public TaskYieldInstruction FetchConnectionDetails() } } - private sealed class EndpointSource : ITokenSourceConfigurable + private sealed class TokenSourceEndpoint : ITokenSourceConfigurable { private string _endpointUrl; IEnumerable _headers; private static readonly HttpClient HttpClient = new HttpClient(); - public EndpointSource(string endpointUrl, IEnumerable headers) + public TokenSourceEndpoint(string endpointUrl, IEnumerable headers) { _endpointUrl = endpointUrl; _headers = headers; From a3cf169aa5838a6884187aa82e5ec4048fb12dc8 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:02:53 +0200 Subject: [PATCH 10/12] Left instructions about deprecated constructors --- Runtime/Scripts/TokenSource/TokenSource.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index 681426d5..f386d5f6 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -227,6 +227,10 @@ public interface ITokenSourceConfigurable : ITokenSource public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options); } + #region Old constructors + // For backwards compatibility the old public constructors are still here but deprecated / obsolete. + // Delete when doing a new major release. + [Obsolete("Use TokenSource.Literal(...) instead")] public class TokenSourceLiteral : ITokenSourceFixed { @@ -278,4 +282,6 @@ public class TokenSourceSandbox : TokenSourceEndpoint public TokenSourceSandbox(string sandboxId) : base(TokenSource.DevelopmentTokenServerUrl, new[] { new StringPair { key = "X-Sandbox-ID", value = sandboxId } }) {} } + + #endregion } From 178825f66354deb626fae36f943ec3b6534653a3 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:37:22 +0200 Subject: [PATCH 11/12] One nit in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 75a3c189..284dee4c 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ To help getting started with tokens, use `TokenSourceComponent.cs` with a `Token #### 1. Literal Use this to pass a pregenerated server URL and token. Generate tokens via the [LiveKit CLI](https://docs.livekit.io/frontends/build/authentication/custom/#manual-token-creation) or from your [LiveKit Cloud](https://cloud.livekit.io/) project's API key page. -#### 2. Development +#### 2. Development Token Server For development and testing. Follow the [development token server guide](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/) to enable your project's development token server and get the token server ID. Optional connection fields (room name, participant name, agent name, etc.) can be configured in the inspector — leave blank for server defaults. #### 3. Endpoint From 56fa01bd475db678cd64c6f638d12fa2dce2caed Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:27:56 +0200 Subject: [PATCH 12/12] Address review feedback: readonly fields, header snapshot, dispose HTTP objects - Make private token source fields readonly - Copy endpoint headers into an IReadOnlyList at construction so later mutation of the caller's collection (e.g. the component's serialized list) no longer leaks into requests - Dispose HttpRequestMessage and HttpResponseMessage via using var Co-Authored-By: Claude Fable 5 --- Runtime/Scripts/TokenSource/TokenSource.cs | 25 ++++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/Runtime/Scripts/TokenSource/TokenSource.cs b/Runtime/Scripts/TokenSource/TokenSource.cs index f386d5f6..24b308cc 100644 --- a/Runtime/Scripts/TokenSource/TokenSource.cs +++ b/Runtime/Scripts/TokenSource/TokenSource.cs @@ -68,8 +68,8 @@ public static ITokenSourceConfigurable DevelopmentTokenServer(string tokenServer private sealed class TokenSourceLiteral : ITokenSourceFixed { - private string _serverUrl; - private string _participantToken; + private readonly string _serverUrl; + private readonly string _participantToken; public TokenSourceLiteral(string serverUrl, string participantToken) { @@ -86,7 +86,7 @@ public TaskYieldInstruction FetchConnectionDetails() private sealed class TokenSourceCustom : ITokenSourceFixed { - private CustomTokenFunction _customTokenFunction; + private readonly CustomTokenFunction _customTokenFunction; public TokenSourceCustom(CustomTokenFunction customTokenFunction) { @@ -114,14 +114,14 @@ public TaskYieldInstruction FetchConnectionDetails() private sealed class TokenSourceEndpoint : ITokenSourceConfigurable { - private string _endpointUrl; - IEnumerable _headers; + private readonly string _endpointUrl; + private readonly IReadOnlyList _headers; private static readonly HttpClient HttpClient = new HttpClient(); public TokenSourceEndpoint(string endpointUrl, IEnumerable headers) { _endpointUrl = endpointUrl; - _headers = headers; + _headers = headers?.ToList() ?? (IReadOnlyList)Array.Empty(); } public TaskYieldInstruction FetchConnectionDetails(TokenSourceFetchOptions options) @@ -136,20 +136,17 @@ private async Task FetchConnectionDetailsAsync(TokenSourceFet var requestBody = BuildRequest(options); var jsonBody = JsonConvert.SerializeObject(requestBody); - var request = new HttpRequestMessage(HttpMethod.Post, _endpointUrl); - if (_headers != null) + using var request = new HttpRequestMessage(HttpMethod.Post, _endpointUrl); + foreach (var header in _headers) { - foreach (var header in _headers) - { - if (!string.IsNullOrEmpty(header.key)) - request.Headers.TryAddWithoutValidation(header.key, header.value); - } + if (!string.IsNullOrEmpty(header.key)) + request.Headers.TryAddWithoutValidation(header.key, header.value); } var content = new StringContent(jsonBody, System.Text.Encoding.UTF8); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); request.Content = content; - var response = await HttpClient.SendAsync(request); + using var response = await HttpClient.SendAsync(request); if (!response.IsSuccessStatusCode) throw new InvalidOperationException($"Token server error: {response.StatusCode}, response: {await response.Content.ReadAsStringAsync()}");