Auth/NetworkStorageDedicatedServerSecretValidation.cs
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;

namespace Sandbox;

public static partial class NetworkStorage
{
	private static Task<bool> _dedicatedSecretValidationTask;

	/// <summary>
	/// Validates the configured dedicated-server secret key against the management API.
	/// No-op outside dedicated server hosts. Logs success/failure without exposing the key.
	/// </summary>
	public static Task<bool> EnsureDedicatedServerSecretValidatedAsync( string reason = "startup", bool forceRefresh = false )
	{
		if ( !IsDedicatedServerHost )
			return Task.FromResult( true );

		if ( !forceRefresh && _dedicatedSecretValidationTask is not null )
			return _dedicatedSecretValidationTask;

		_dedicatedSecretValidationTask = ValidateDedicatedServerSecretAsync( reason );
		return _dedicatedSecretValidationTask;
	}

	private static async Task<bool> ValidateDedicatedServerSecretAsync( string reason )
	{
		EnsureConfigured();
		ClearLastEndpointError( "dedicated-secret" );

		if ( !TryGetDedicatedServerSecretKey( null, out var secretKey ) )
		{
			LogDedicatedSecretMissingOnce();
			RecordEndpointError( "dedicated-secret", "DEDICATED_SECRET_REQUIRED", DedicatedServerAuthUnavailableMessage() );
			return false;
		}

		if ( !CanTransportDedicatedServerSecretSecurely() )
		{
			LogInsecureSecretTransportOnce();
			RecordEndpointError( "dedicated-secret", "DEDICATED_SECRET_INSECURE_TRANSPORT", "Dedicated server secret keys are only sent to HTTPS or loopback API roots." );
			return false;
		}

		if ( IsPlaceholderSecretKey( secretKey ) )
		{
			var message = "Dedicated server secret key looks like the example placeholder; use a real sbox_sk_ key from the dashboard.";
			_dedicatedServerSecretKeyRejected = true;
			Log.Warning( $"[NetworkStorage] {message}" );
			RecordEndpointError( "dedicated-secret", "DEDICATED_SECRET_PLACEHOLDER", message );
			return false;
		}

		var path = $"/manage/{EscapeRouteSegment( ProjectId )}/validate";
		var url = $"{ApiRoot}{path}";
		var headers = new Dictionary<string, string>
		{
			["x-api-key"] = secretKey,
			["x-public-key"] = ApiKey ?? ""
		};

		try
		{
			Log.Info( $"[NetworkStorage] Validating dedicated server secret key source={DedicatedServerSecretKeySource} reason={reason}..." );

			var raw = await Http.RequestStringAsync( url, "GET", null, headers );
			using var doc = JsonDocument.Parse( raw );
			var root = doc.RootElement;
			if ( !DedicatedSecretValidationSucceeded( root, raw, out var detail ) )
			{
				var message = string.IsNullOrWhiteSpace( detail ) ? "Dedicated server secret key validation failed." : detail;
				_dedicatedServerSecretKeyRejected = true;
				Log.Warning( $"[NetworkStorage] Dedicated server secret key validation failed: {message}" );
				RecordEndpointError( "dedicated-secret", "DEDICATED_SECRET_INVALID", message );
				return false;
			}

			_dedicatedServerSecretKeyRejected = false;
			var project = ReadValidationProjectName( root );
			var suffix = string.IsNullOrWhiteSpace( project ) ? "" : $" project={project}";
			Log.Info( $"[NetworkStorage] Dedicated server secret key validated source={DedicatedServerSecretKeySource}{suffix}." );
			return true;
		}
		catch ( Exception ex )
		{
			Log.Warning( $"[NetworkStorage] Dedicated server secret key validation failed: {ex.Message}" );
			RecordEndpointError( "dedicated-secret", "DEDICATED_SECRET_VALIDATE_FAILED", ex.Message );
			return false;
		}
	}

	private static bool DedicatedSecretValidationSucceeded( JsonElement root, string raw, out string detail )
	{
		detail = ReadValidationFailureDetail( root, raw );
		if ( root.TryGetProperty( "error", out var error ) && error.ValueKind != JsonValueKind.Undefined )
			return false;
		if ( root.TryGetProperty( "ok", out var ok ) && ok.ValueKind == JsonValueKind.False )
			return false;

		if ( root.TryGetProperty( "checks", out var checks ) && checks.ValueKind == JsonValueKind.Object &&
			checks.TryGetProperty( "secretKey", out var secretCheck ) && secretCheck.ValueKind == JsonValueKind.Object )
		{
			if ( secretCheck.TryGetProperty( "message", out var message ) && message.ValueKind == JsonValueKind.String )
				detail = message.GetString();
			if ( secretCheck.TryGetProperty( "ok", out var checkOk ) && checkOk.ValueKind == JsonValueKind.False )
				return false;
			if ( secretCheck.TryGetProperty( "valid", out var valid ) && valid.ValueKind == JsonValueKind.False )
				return false;
			if ( secretCheck.TryGetProperty( "status", out var status ) && status.ValueKind == JsonValueKind.String )
			{
				var value = status.GetString();
				if ( string.Equals( value, "invalid", StringComparison.OrdinalIgnoreCase ) ||
					string.Equals( value, "failed", StringComparison.OrdinalIgnoreCase ) ||
					string.Equals( value, "error", StringComparison.OrdinalIgnoreCase ) )
				{
					return false;
				}
			}
		}

		return true;
	}

	private static string ReadValidationFailureDetail( JsonElement root, string raw )
	{
		if ( root.TryGetProperty( "error", out var error ) )
		{
			if ( error.ValueKind == JsonValueKind.Object )
			{
				var code = error.TryGetProperty( "code", out var codeElement ) && codeElement.ValueKind == JsonValueKind.String
					? codeElement.GetString()
					: null;
				var message = error.TryGetProperty( "message", out var messageElement ) && messageElement.ValueKind == JsonValueKind.String
					? messageElement.GetString()
					: null;
				if ( !string.IsNullOrWhiteSpace( code ) || !string.IsNullOrWhiteSpace( message ) )
					return string.IsNullOrWhiteSpace( code ) ? message : string.IsNullOrWhiteSpace( message ) ? code : $"{code}: {message}";
			}
			else if ( error.ValueKind == JsonValueKind.String )
			{
				return error.GetString();
			}
		}

		var serverMessage = ReadServerMessage( root, null );
		if ( !string.IsNullOrWhiteSpace( serverMessage ) )
			return serverMessage;

		return string.IsNullOrWhiteSpace( raw ) ? null : $"Unexpected validate response: {TruncateJson( raw, 300 )}";
	}

	private static bool IsPlaceholderSecretKey( string secretKey )
	{
		if ( string.IsNullOrWhiteSpace( secretKey ) )
			return false;

		var normalized = secretKey.Trim();
		return string.Equals( normalized, "sbox_sk_your_secret_key", StringComparison.OrdinalIgnoreCase )
			|| normalized.Contains( "your_secret", StringComparison.OrdinalIgnoreCase )
			|| normalized.Contains( "your-secret", StringComparison.OrdinalIgnoreCase );
	}

	private static string ReadValidationProjectName( JsonElement root )
	{
		if ( root.TryGetProperty( "project", out var project ) && project.ValueKind == JsonValueKind.Object )
		{
			if ( project.TryGetProperty( "name", out var name ) && name.ValueKind == JsonValueKind.String )
				return name.GetString();
			if ( project.TryGetProperty( "title", out var title ) && title.ValueKind == JsonValueKind.String )
				return title.GetString();
		}

		return null;
	}
}