diff --git a/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets b/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets
index 5673185eaf3..8e56ebe7b00 100644
--- a/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets
+++ b/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets
@@ -9,6 +9,7 @@
<_NitroIdentityClientId>$(NitroIdentityClientId)
<_NitroIdentityScopes>$(NitroIdentityScopes)
+ <_NitroApiClientId>$(NitroApiClientId)
@@ -23,6 +24,7 @@ namespace $(_SecretsNamespace)
{
internal const string NitroIdentityClientId = "$(_NitroIdentityClientId)";
internal const string NitroIdentityScopes = "$(_NitroIdentityScopes)";
+ internal const string NitroApiClientId = "$(_NitroApiClientId)";
}
}
))" />
diff --git a/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs b/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs
index e884528e0fc..1c2bfd7740c 100644
--- a/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs
+++ b/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs
@@ -16,6 +16,8 @@ public Uri Url
public INitroClientAuthorization? Authorization { get; private set; }
+ public string? ClientId => BuildSecrets.NitroApiClientId;
+
public void Configure(string? apiUrl, INitroClientAuthorization? authorization)
{
if (string.IsNullOrWhiteSpace(apiUrl))
diff --git a/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs b/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs
index e7a3871a3e9..9b56ccdfb12 100644
--- a/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs
+++ b/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs
@@ -67,6 +67,57 @@ public async Task ExecuteAsync_Should_PreferApiKey_Over_SessionToken()
Assert.False(client.DefaultRequestHeaders.Contains("Authorization"));
}
+ [Fact]
+ public async Task ExecuteAsync_Should_ConfigureApiKeyAuth_When_EnvApiKeySet()
+ {
+ // arrange
+ var envProvider = CreateEnvironmentProvider(apiKey: "env-key");
+
+ // act
+ await using var provider = await BuildAndExecuteAsync([], environmentVariables: envProvider);
+ using var client = CreateApiClient(provider);
+
+ // assert
+ var apiKeyHeader = Assert.Single(client.DefaultRequestHeaders.GetValues("CCC-api-key"));
+ Assert.Equal("env-key", apiKeyHeader);
+ Assert.False(client.DefaultRequestHeaders.Contains("Authorization"));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Should_PreferEnvApiKey_Over_SessionToken()
+ {
+ // arrange
+ var session = CreateSessionWithTokens(accessToken: "session-token");
+ var envProvider = CreateEnvironmentProvider(apiKey: "env-key");
+
+ // act
+ await using var provider = await BuildAndExecuteAsync(
+ [], session, environmentVariables: envProvider);
+ using var client = CreateApiClient(provider);
+
+ // assert
+ var apiKeyHeader = Assert.Single(client.DefaultRequestHeaders.GetValues("CCC-api-key"));
+ Assert.Equal("env-key", apiKeyHeader);
+ Assert.False(client.DefaultRequestHeaders.Contains("Authorization"));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Should_PreferExplicitApiKey_Over_EnvApiKey()
+ {
+ // arrange
+ var envProvider = CreateEnvironmentProvider(apiKey: "env-key");
+
+ // act
+ await using var provider = await BuildAndExecuteAsync(
+ ["--api-key", "cli-key"], environmentVariables: envProvider);
+ using var client = CreateApiClient(provider);
+
+ // assert
+ var apiKeyHeader = Assert.Single(client.DefaultRequestHeaders.GetValues("CCC-api-key"));
+ Assert.Equal("cli-key", apiKeyHeader);
+ Assert.False(client.DefaultRequestHeaders.Contains("Authorization"));
+ }
+
[Theory]
[InlineData("custom.host.com", "https://custom.host.com/graphql")]
[InlineData("https://custom.host.com", "https://custom.host.com/graphql")]
@@ -239,6 +290,13 @@ private static HttpClient CreateApiClient(ServiceProvider provider)
return factory.CreateClient(ApiClient.ClientName);
}
+ private static IEnvironmentVariableProvider CreateEnvironmentProvider(string? apiKey = null)
+ {
+ var mock = new Mock();
+ mock.Setup(x => x.GetEnvironmentVariable("NITRO_API_KEY")).Returns(apiKey);
+ return mock.Object;
+ }
+
private static Session CreateSessionWithTokens(
string apiUrl = "api.chillicream.com",
string accessToken = "test-access-token")
diff --git a/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs b/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs
index 8ec17671292..01ce3fe1648 100644
--- a/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs
+++ b/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs
@@ -195,6 +195,12 @@ private static void ConfigureApiHttpClient(IServiceProvider sp, HttpClient clien
var provider = sp.GetRequiredService();
client.BaseAddress = provider.Url;
+ if (!string.IsNullOrWhiteSpace(provider.ClientId))
+ {
+ client.DefaultRequestHeaders.Remove(NitroClientHeaders.GraphQLClientId);
+ client.DefaultRequestHeaders.Add(NitroClientHeaders.GraphQLClientId, provider.ClientId);
+ }
+
switch (provider.Authorization)
{
case NitroClientApiKeyAuthorization apiKey:
diff --git a/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs b/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs
index 9e83b65e6fd..01a1c6bb933 100644
--- a/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs
+++ b/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs
@@ -5,4 +5,6 @@ public interface INitroClientContextProvider
Uri Url { get; }
INitroClientAuthorization? Authorization { get; }
+
+ string? ClientId { get; }
}
diff --git a/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs b/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs
index 0d2ceca48de..d2f98a90994 100644
--- a/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs
+++ b/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs
@@ -2,6 +2,7 @@ namespace ChilliCream.Nitro.Client;
public static class NitroClientHeaders
{
+ public const string GraphQLClientId = "GraphQL-Client-Id";
public const string GraphQLClientVersion = "GraphQL-Client-Version";
public const string GraphQLPreflight = "GraphQL-Preflight";
public const string CccAgent = "ccc-agent";