Code/Core/NetworkStorageHttp.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Sandbox;

public static partial class NetworkStorage
{
	public static void EnsureConfigured()
	{
		if ( !IsConfigured )
			AutoConfigure();

		if ( !IsConfigured )
			throw new InvalidOperationException( "NetworkStorage not configured. Add credentials via Editor → Network Storage → Setup, or call NetworkStorage.Configure() manually." );

		NetworkStorageAnalyticsRuntime.EnsureCreated( "ensure-configured" );
	}

	private static bool IsCdnRoot( string root )
		=> root.Contains( "storage.sbox.cool" ) || root.Contains( "storage.sboxcool.com" );

	/// <summary>
	/// Build the request URL with API key query param (no auth tokens in URL).
	/// </summary>
	private static string BuildUrl( string path, bool includeDedicatedSecretFlag = false )
	{
		var baseUrl = $"{ApiRoot}{path}?apiKey={Uri.EscapeDataString( ApiKey )}";
		if ( includeDedicatedSecretFlag )
			baseUrl += "&secret-key=1";
		if ( string.Equals( PublishTarget, "next", StringComparison.OrdinalIgnoreCase ) )
			baseUrl += "&revisionTarget=next&includeStaged=true";
		if ( IsCdnRoot( ApiRoot ) )
		{
			var v = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			return $"{baseUrl}&v={v}";
		}
		return baseUrl;
	}

	/// <summary>
	/// Build auth headers containing the Steam ID and auth token.
	/// Sending auth via headers avoids URL-encoding issues that corrupt
	/// base64 tokens in query strings (+ decoded as space, etc.).
	/// </summary>
	private static Dictionary<string, string> BuildPublicHeaders( string clientMode = null )
	{
		var headers = new Dictionary<string, string>
		{
			{ "x-public-key", ApiKey ?? "" }
		};
		if ( !string.IsNullOrWhiteSpace( clientMode ) )
			headers["x-ns-security-mode"] = clientMode;
		if ( !string.IsNullOrWhiteSpace( RuntimeSecurityConfigVersion ) )
			headers["x-ns-security-config-version"] = RuntimeSecurityConfigVersion;

		AddRuntimeTargetHeaders( headers );
		return headers;
	}

	private static async Task<Dictionary<string, string>> BuildAuthHeaders( string authSessionToken = null, string clientMode = null )
	{
		if ( IsDedicatedServerProcess )
		{
			LogDedicatedPlayerAuthSuppressedOnce();
			return BuildPublicHeaders( clientMode );
		}

		var steamId = Game.SteamId.ToString();
		var token = TryTakePreparedAuthToken() ?? await GetAuthTokenWithRetry( $"steamId={steamId}" );

		if ( string.IsNullOrEmpty( token ) )
		{
			if ( NetworkStorageLogConfig.LogTokens )
				Log.Warning( $"[NetworkStorage] Auth token is empty for steamId={steamId} — requests may fail" );
		}
		else if ( NetworkStorageLogConfig.LogTokens )
		{
			var preview = token.Length > 8 ? $"{token[..4]}...{token[^4..]}" : "****";
			Log.Info( $"[NetworkStorage] Auth token acquired for steamId={steamId} ({token.Length} chars, preview={preview})" );
		}

		var headers = new Dictionary<string, string>
		{
			{ "x-public-key", ApiKey ?? "" },
			{ "x-steam-id", steamId },
			{ "x-sbox-token", token ?? "" }
		};
		if ( !string.IsNullOrWhiteSpace( authSessionToken ) )
			headers["x-auth-session"] = authSessionToken;
		if ( !string.IsNullOrWhiteSpace( clientMode ) )
			headers["x-ns-security-mode"] = clientMode;
		if ( !string.IsNullOrWhiteSpace( RuntimeSecurityConfigVersion ) )
			headers["x-ns-security-config-version"] = RuntimeSecurityConfigVersion;

		AddRuntimeTargetHeaders( headers );

		return headers;
	}

	private static void AddRuntimeTargetHeaders( Dictionary<string, string> headers )
	{
		var revisionId = NetworkStoragePackageInfo.RuntimeRevisionId;
		if ( revisionId.HasValue )
			headers["x-ns-revision-id"] = revisionId.Value.ToString();

		if ( string.Equals( PublishTarget, "next", StringComparison.OrdinalIgnoreCase ) )
			headers["x-ns-publish-target"] = "next";

		var clientType = GetClientType();
		if ( !string.IsNullOrEmpty( clientType ) )
			headers["x-ns-client-type"] = clientType;
	}

}