From 7b1f3f35117f35101f07ed55f173ed91a427efb9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 16:05:47 +0000 Subject: [PATCH 1/4] fix(csharp): resolve ambiguity in parsing query strings --- src/Imagekit.Tests/TestBase.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Imagekit.Tests/TestBase.cs b/src/Imagekit.Tests/TestBase.cs index e661d50f..a380804f 100644 --- a/src/Imagekit.Tests/TestBase.cs +++ b/src/Imagekit.Tests/TestBase.cs @@ -38,6 +38,9 @@ internal static bool UrisEqual(Uri uri1, Uri uri2) return Enumerable.SequenceEqual(query1, query2); } + private static readonly char[] _ampersandArray = ['&']; + private static readonly char[] _equalsArray = ['=']; + static SortedDictionary ParseQueryString(string query) { var ret = new SortedDictionary(StringComparer.Ordinal); @@ -45,11 +48,13 @@ static SortedDictionary ParseQueryString(string query) if (string.IsNullOrEmpty(query)) return ret; - var pairs = query.TrimStart('?').Split(['&'], StringSplitOptions.RemoveEmptyEntries); + var pairs = query + .TrimStart('?') + .Split(_ampersandArray, StringSplitOptions.RemoveEmptyEntries); foreach (var pair in pairs) { - var parts = pair.Split(['&'], 2); + var parts = pair.Split(_equalsArray, 2); var key = Uri.UnescapeDataString(parts[0]); var value = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : string.Empty; ret[key] = value; From ae25f9c2480ea5fec242e5f601bb6fb46d9088cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:54:35 +0000 Subject: [PATCH 2/4] feat(origins): add useIAMRole for IAM role authentication Add `useIAMRole` boolean to S3 and Cloudinary Backup origin request and response schemas. When enabled, the origin authenticates via an IAM role and `accessKey`/`secretKey` are sent as empty strings. Update access/secret key descriptions accordingly and drop their `minLength: 1` constraint. --- .stats.yml | 4 +- .../Origins/OriginCreateParamsTest.cs | 4 ++ .../Accounts/Origins/OriginRequestTest.cs | 34 +++++++++ .../Accounts/Origins/OriginResponseTest.cs | 34 +++++++++ .../Origins/OriginUpdateParamsTest.cs | 4 ++ .../Services/Accounts/OriginServiceTest.cs | 2 + .../Models/Accounts/Origins/OriginRequest.cs | 71 +++++++++++++++++-- .../Models/Accounts/Origins/OriginResponse.cs | 63 ++++++++++++++++ 8 files changed, 210 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index 14a49efd..9768080b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 48 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc/imagekit-diversion-ebb91d3a579cbd08d5af02b57042964052e28b703bc0c1ab73270130a7575e89.yml -openapi_spec_hash: 86c6aef81fd1ddb2a989da3155364231 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc/imagekit-diversion-df7d99073b94ff23f887a5bb4e9d85b09e743069e134cd62ec90967069513379.yml +openapi_spec_hash: 94d902416dd182c025a5e3840aa9c2ff config_hash: 66121ffadb78b9866f6b853f19b11f3d diff --git a/src/Imagekit.Tests/Models/Accounts/Origins/OriginCreateParamsTest.cs b/src/Imagekit.Tests/Models/Accounts/Origins/OriginCreateParamsTest.cs index 9bba931b..9b84fc22 100644 --- a/src/Imagekit.Tests/Models/Accounts/Origins/OriginCreateParamsTest.cs +++ b/src/Imagekit.Tests/Models/Accounts/Origins/OriginCreateParamsTest.cs @@ -19,6 +19,7 @@ public void FieldRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }; @@ -31,6 +32,7 @@ public void FieldRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }; Assert.Equal(expectedOriginRequest, parameters.OriginRequest); @@ -50,6 +52,7 @@ public void Url_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }; @@ -74,6 +77,7 @@ public void CopyConstructor_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }; diff --git a/src/Imagekit.Tests/Models/Accounts/Origins/OriginRequestTest.cs b/src/Imagekit.Tests/Models/Accounts/Origins/OriginRequestTest.cs index fb226ecf..7bec45d3 100644 --- a/src/Imagekit.Tests/Models/Accounts/Origins/OriginRequestTest.cs +++ b/src/Imagekit.Tests/Models/Accounts/Origins/OriginRequestTest.cs @@ -18,6 +18,7 @@ public void S3ValidationWorks() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; value.Validate(); } @@ -52,6 +53,7 @@ public void CloudinaryBackupValidationWorks() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; value.Validate(); } @@ -143,6 +145,7 @@ public void S3SerializationRoundtripWorks() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); var deserialized = JsonSerializer.Deserialize( @@ -189,6 +192,7 @@ public void CloudinaryBackupSerializationRoundtripWorks() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); var deserialized = JsonSerializer.Deserialize( @@ -319,6 +323,7 @@ public void FieldRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string expectedAccessKey = "AKIAIOSFODNN7EXAMPLE"; @@ -329,6 +334,7 @@ public void FieldRoundtrip_Works() string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; bool expectedIncludeCanonicalHeader = false; string expectedPrefix = "raw-assets"; + bool expectedUseIamRole = true; Assert.Equal(expectedAccessKey, model.AccessKey); Assert.Equal(expectedBucket, model.Bucket); @@ -338,6 +344,7 @@ public void FieldRoundtrip_Works() Assert.Equal(expectedBaseUrlForCanonicalHeader, model.BaseUrlForCanonicalHeader); Assert.Equal(expectedIncludeCanonicalHeader, model.IncludeCanonicalHeader); Assert.Equal(expectedPrefix, model.Prefix); + Assert.Equal(expectedUseIamRole, model.UseIamRole); } [Fact] @@ -352,6 +359,7 @@ public void SerializationRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string json = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -372,6 +380,7 @@ public void FieldRoundtripThroughSerialization_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string element = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -386,6 +395,7 @@ public void FieldRoundtripThroughSerialization_Works() string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; bool expectedIncludeCanonicalHeader = false; string expectedPrefix = "raw-assets"; + bool expectedUseIamRole = true; Assert.Equal(expectedAccessKey, deserialized.AccessKey); Assert.Equal(expectedBucket, deserialized.Bucket); @@ -395,6 +405,7 @@ public void FieldRoundtripThroughSerialization_Works() Assert.Equal(expectedBaseUrlForCanonicalHeader, deserialized.BaseUrlForCanonicalHeader); Assert.Equal(expectedIncludeCanonicalHeader, deserialized.IncludeCanonicalHeader); Assert.Equal(expectedPrefix, deserialized.Prefix); + Assert.Equal(expectedUseIamRole, deserialized.UseIamRole); } [Fact] @@ -409,6 +420,7 @@ public void Validation_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; model.Validate(); @@ -431,6 +443,8 @@ public void OptionalNonNullablePropertiesUnsetAreNotSet_Works() Assert.False(model.RawData.ContainsKey("includeCanonicalHeader")); Assert.Null(model.Prefix); Assert.False(model.RawData.ContainsKey("prefix")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -461,6 +475,7 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() BaseUrlForCanonicalHeader = null, IncludeCanonicalHeader = null, Prefix = null, + UseIamRole = null, }; Assert.Null(model.BaseUrlForCanonicalHeader); @@ -469,6 +484,8 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() Assert.False(model.RawData.ContainsKey("includeCanonicalHeader")); Assert.Null(model.Prefix); Assert.False(model.RawData.ContainsKey("prefix")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -485,6 +502,7 @@ public void OptionalNonNullablePropertiesSetToNullValidation_Works() BaseUrlForCanonicalHeader = null, IncludeCanonicalHeader = null, Prefix = null, + UseIamRole = null, }; model.Validate(); @@ -502,6 +520,7 @@ public void CopyConstructor_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; S3 copied = new(model); @@ -763,6 +782,7 @@ public void FieldRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string expectedAccessKey = "AKIAIOSFODNN7EXAMPLE"; @@ -773,6 +793,7 @@ public void FieldRoundtrip_Works() string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; bool expectedIncludeCanonicalHeader = false; string expectedPrefix = "raw-assets"; + bool expectedUseIamRole = true; Assert.Equal(expectedAccessKey, model.AccessKey); Assert.Equal(expectedBucket, model.Bucket); @@ -782,6 +803,7 @@ public void FieldRoundtrip_Works() Assert.Equal(expectedBaseUrlForCanonicalHeader, model.BaseUrlForCanonicalHeader); Assert.Equal(expectedIncludeCanonicalHeader, model.IncludeCanonicalHeader); Assert.Equal(expectedPrefix, model.Prefix); + Assert.Equal(expectedUseIamRole, model.UseIamRole); } [Fact] @@ -796,6 +818,7 @@ public void SerializationRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string json = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -819,6 +842,7 @@ public void FieldRoundtripThroughSerialization_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; string element = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -836,6 +860,7 @@ public void FieldRoundtripThroughSerialization_Works() string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; bool expectedIncludeCanonicalHeader = false; string expectedPrefix = "raw-assets"; + bool expectedUseIamRole = true; Assert.Equal(expectedAccessKey, deserialized.AccessKey); Assert.Equal(expectedBucket, deserialized.Bucket); @@ -845,6 +870,7 @@ public void FieldRoundtripThroughSerialization_Works() Assert.Equal(expectedBaseUrlForCanonicalHeader, deserialized.BaseUrlForCanonicalHeader); Assert.Equal(expectedIncludeCanonicalHeader, deserialized.IncludeCanonicalHeader); Assert.Equal(expectedPrefix, deserialized.Prefix); + Assert.Equal(expectedUseIamRole, deserialized.UseIamRole); } [Fact] @@ -859,6 +885,7 @@ public void Validation_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; model.Validate(); @@ -881,6 +908,8 @@ public void OptionalNonNullablePropertiesUnsetAreNotSet_Works() Assert.False(model.RawData.ContainsKey("includeCanonicalHeader")); Assert.Null(model.Prefix); Assert.False(model.RawData.ContainsKey("prefix")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -911,6 +940,7 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() BaseUrlForCanonicalHeader = null, IncludeCanonicalHeader = null, Prefix = null, + UseIamRole = null, }; Assert.Null(model.BaseUrlForCanonicalHeader); @@ -919,6 +949,8 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() Assert.False(model.RawData.ContainsKey("includeCanonicalHeader")); Assert.Null(model.Prefix); Assert.False(model.RawData.ContainsKey("prefix")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -935,6 +967,7 @@ public void OptionalNonNullablePropertiesSetToNullValidation_Works() BaseUrlForCanonicalHeader = null, IncludeCanonicalHeader = null, Prefix = null, + UseIamRole = null, }; model.Validate(); @@ -952,6 +985,7 @@ public void CopyConstructor_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "raw-assets", + UseIamRole = true, }; CloudinaryBackup copied = new(model); diff --git a/src/Imagekit.Tests/Models/Accounts/Origins/OriginResponseTest.cs b/src/Imagekit.Tests/Models/Accounts/Origins/OriginResponseTest.cs index 8f9e7720..6b38bb19 100644 --- a/src/Imagekit.Tests/Models/Accounts/Origins/OriginResponseTest.cs +++ b/src/Imagekit.Tests/Models/Accounts/Origins/OriginResponseTest.cs @@ -17,6 +17,7 @@ public void S3ValidationWorks() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; value.Validate(); } @@ -49,6 +50,7 @@ public void CloudinaryBackupValidationWorks() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; value.Validate(); } @@ -138,6 +140,7 @@ public void S3SerializationRoundtripWorks() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); var deserialized = JsonSerializer.Deserialize( @@ -182,6 +185,7 @@ public void CloudinaryBackupSerializationRoundtripWorks() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); var deserialized = JsonSerializer.Deserialize( @@ -310,6 +314,7 @@ public void FieldRoundtrip_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string expectedID = "id"; @@ -319,6 +324,7 @@ public void FieldRoundtrip_Works() string expectedPrefix = "raw-assets"; JsonElement expectedType = JsonSerializer.SerializeToElement("S3"); string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; + bool expectedUseIamRole = true; Assert.Equal(expectedID, model.ID); Assert.Equal(expectedBucket, model.Bucket); @@ -327,6 +333,7 @@ public void FieldRoundtrip_Works() Assert.Equal(expectedPrefix, model.Prefix); Assert.True(JsonElement.DeepEquals(expectedType, model.Type)); Assert.Equal(expectedBaseUrlForCanonicalHeader, model.BaseUrlForCanonicalHeader); + Assert.Equal(expectedUseIamRole, model.UseIamRole); } [Fact] @@ -340,6 +347,7 @@ public void SerializationRoundtrip_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string json = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -362,6 +370,7 @@ public void FieldRoundtripThroughSerialization_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string element = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -378,6 +387,7 @@ public void FieldRoundtripThroughSerialization_Works() string expectedPrefix = "raw-assets"; JsonElement expectedType = JsonSerializer.SerializeToElement("S3"); string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; + bool expectedUseIamRole = true; Assert.Equal(expectedID, deserialized.ID); Assert.Equal(expectedBucket, deserialized.Bucket); @@ -386,6 +396,7 @@ public void FieldRoundtripThroughSerialization_Works() Assert.Equal(expectedPrefix, deserialized.Prefix); Assert.True(JsonElement.DeepEquals(expectedType, deserialized.Type)); Assert.Equal(expectedBaseUrlForCanonicalHeader, deserialized.BaseUrlForCanonicalHeader); + Assert.Equal(expectedUseIamRole, deserialized.UseIamRole); } [Fact] @@ -399,6 +410,7 @@ public void Validation_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; model.Validate(); @@ -418,6 +430,8 @@ public void OptionalNonNullablePropertiesUnsetAreNotSet_Works() Assert.Null(model.BaseUrlForCanonicalHeader); Assert.False(model.RawData.ContainsKey("baseUrlForCanonicalHeader")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -448,10 +462,13 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() // Null should be interpreted as omitted for these properties BaseUrlForCanonicalHeader = null, + UseIamRole = null, }; Assert.Null(model.BaseUrlForCanonicalHeader); Assert.False(model.RawData.ContainsKey("baseUrlForCanonicalHeader")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -467,6 +484,7 @@ public void OptionalNonNullablePropertiesSetToNullValidation_Works() // Null should be interpreted as omitted for these properties BaseUrlForCanonicalHeader = null, + UseIamRole = null, }; model.Validate(); @@ -483,6 +501,7 @@ public void CopyConstructor_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; OriginResponseS3 copied = new(model); @@ -724,6 +743,7 @@ public void FieldRoundtrip_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string expectedID = "id"; @@ -733,6 +753,7 @@ public void FieldRoundtrip_Works() string expectedPrefix = "raw-assets"; JsonElement expectedType = JsonSerializer.SerializeToElement("CLOUDINARY_BACKUP"); string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; + bool expectedUseIamRole = true; Assert.Equal(expectedID, model.ID); Assert.Equal(expectedBucket, model.Bucket); @@ -741,6 +762,7 @@ public void FieldRoundtrip_Works() Assert.Equal(expectedPrefix, model.Prefix); Assert.True(JsonElement.DeepEquals(expectedType, model.Type)); Assert.Equal(expectedBaseUrlForCanonicalHeader, model.BaseUrlForCanonicalHeader); + Assert.Equal(expectedUseIamRole, model.UseIamRole); } [Fact] @@ -754,6 +776,7 @@ public void SerializationRoundtrip_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string json = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -776,6 +799,7 @@ public void FieldRoundtripThroughSerialization_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; string element = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -792,6 +816,7 @@ public void FieldRoundtripThroughSerialization_Works() string expectedPrefix = "raw-assets"; JsonElement expectedType = JsonSerializer.SerializeToElement("CLOUDINARY_BACKUP"); string expectedBaseUrlForCanonicalHeader = "https://cdn.example.com"; + bool expectedUseIamRole = true; Assert.Equal(expectedID, deserialized.ID); Assert.Equal(expectedBucket, deserialized.Bucket); @@ -800,6 +825,7 @@ public void FieldRoundtripThroughSerialization_Works() Assert.Equal(expectedPrefix, deserialized.Prefix); Assert.True(JsonElement.DeepEquals(expectedType, deserialized.Type)); Assert.Equal(expectedBaseUrlForCanonicalHeader, deserialized.BaseUrlForCanonicalHeader); + Assert.Equal(expectedUseIamRole, deserialized.UseIamRole); } [Fact] @@ -813,6 +839,7 @@ public void Validation_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; model.Validate(); @@ -832,6 +859,8 @@ public void OptionalNonNullablePropertiesUnsetAreNotSet_Works() Assert.Null(model.BaseUrlForCanonicalHeader); Assert.False(model.RawData.ContainsKey("baseUrlForCanonicalHeader")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -862,10 +891,13 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() // Null should be interpreted as omitted for these properties BaseUrlForCanonicalHeader = null, + UseIamRole = null, }; Assert.Null(model.BaseUrlForCanonicalHeader); Assert.False(model.RawData.ContainsKey("baseUrlForCanonicalHeader")); + Assert.Null(model.UseIamRole); + Assert.False(model.RawData.ContainsKey("useIAMRole")); } [Fact] @@ -881,6 +913,7 @@ public void OptionalNonNullablePropertiesSetToNullValidation_Works() // Null should be interpreted as omitted for these properties BaseUrlForCanonicalHeader = null, + UseIamRole = null, }; model.Validate(); @@ -897,6 +930,7 @@ public void CopyConstructor_Works() Name = "US S3 Storage", Prefix = "raw-assets", BaseUrlForCanonicalHeader = "https://cdn.example.com", + UseIamRole = true, }; OriginResponseCloudinaryBackup copied = new(model); diff --git a/src/Imagekit.Tests/Models/Accounts/Origins/OriginUpdateParamsTest.cs b/src/Imagekit.Tests/Models/Accounts/Origins/OriginUpdateParamsTest.cs index 8a287a05..339c5b60 100644 --- a/src/Imagekit.Tests/Models/Accounts/Origins/OriginUpdateParamsTest.cs +++ b/src/Imagekit.Tests/Models/Accounts/Origins/OriginUpdateParamsTest.cs @@ -20,6 +20,7 @@ public void FieldRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }; @@ -33,6 +34,7 @@ public void FieldRoundtrip_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }; Assert.Equal(expectedID, parameters.ID); @@ -54,6 +56,7 @@ public void Url_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }; @@ -79,6 +82,7 @@ public void CopyConstructor_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }; diff --git a/src/Imagekit.Tests/Services/Accounts/OriginServiceTest.cs b/src/Imagekit.Tests/Services/Accounts/OriginServiceTest.cs index c8a31cff..8f335652 100644 --- a/src/Imagekit.Tests/Services/Accounts/OriginServiceTest.cs +++ b/src/Imagekit.Tests/Services/Accounts/OriginServiceTest.cs @@ -20,6 +20,7 @@ public async Task Create_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }, TestContext.Current.CancellationToken @@ -43,6 +44,7 @@ public async Task Update_Works() BaseUrlForCanonicalHeader = "https://cdn.example.com", IncludeCanonicalHeader = false, Prefix = "images", + UseIamRole = true, }, }, TestContext.Current.CancellationToken diff --git a/src/Imagekit/Models/Accounts/Origins/OriginRequest.cs b/src/Imagekit/Models/Accounts/Origins/OriginRequest.cs index c1b6bc07..726d7fb8 100644 --- a/src/Imagekit/Models/Accounts/Origins/OriginRequest.cs +++ b/src/Imagekit/Models/Accounts/Origins/OriginRequest.cs @@ -166,6 +166,23 @@ public string? Prefix } } + public bool? UseIamRole + { + get + { + return Match( + s3: (x) => x.UseIamRole, + s3Compatible: (_) => null, + cloudinaryBackup: (x) => x.UseIamRole, + webFolder: (_) => null, + webProxy: (_) => null, + gcs: (_) => null, + azureBlob: (_) => null, + akeneoPim: (_) => null + ); + } + } + public string? BaseUrl { get @@ -788,7 +805,7 @@ JsonSerializerOptions options public sealed record class S3 : JsonModel { /// - /// Access key for the bucket. + /// Access key for the bucket. When `useIAMRole` is `true`, send an empty string. /// public required string AccessKey { @@ -827,7 +844,7 @@ public required string Name } /// - /// Secret key for the bucket. + /// Secret key for the bucket. When `useIAMRole` is `true`, send an empty string. /// public required string SecretKey { @@ -912,6 +929,28 @@ public string? Prefix } } + /// + /// Use IAM role for authentication instead of access/secret keys. When set to + /// `true`, send an empty string for both `accessKey` and `secretKey`. + /// + public bool? UseIamRole + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNullableStruct("useIAMRole"); + } + init + { + if (value == null) + { + return; + } + + this._rawData.Set("useIAMRole", value); + } + } + /// public override void Validate() { @@ -926,6 +965,7 @@ public override void Validate() _ = this.BaseUrlForCanonicalHeader; _ = this.IncludeCanonicalHeader; _ = this.Prefix; + _ = this.UseIamRole; } public S3() @@ -1192,7 +1232,7 @@ public S3Compatible FromRawUnchecked(IReadOnlyDictionary ra public sealed record class CloudinaryBackup : JsonModel { /// - /// Access key for the bucket. + /// Access key for the bucket. When `useIAMRole` is `true`, send an empty string. /// public required string AccessKey { @@ -1231,7 +1271,7 @@ public required string Name } /// - /// Secret key for the bucket. + /// Secret key for the bucket. When `useIAMRole` is `true`, send an empty string. /// public required string SecretKey { @@ -1316,6 +1356,28 @@ public string? Prefix } } + /// + /// Use IAM role for authentication instead of access/secret keys. When set to + /// `true`, send an empty string for both `accessKey` and `secretKey`. + /// + public bool? UseIamRole + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNullableStruct("useIAMRole"); + } + init + { + if (value == null) + { + return; + } + + this._rawData.Set("useIAMRole", value); + } + } + /// public override void Validate() { @@ -1335,6 +1397,7 @@ public override void Validate() _ = this.BaseUrlForCanonicalHeader; _ = this.IncludeCanonicalHeader; _ = this.Prefix; + _ = this.UseIamRole; } public CloudinaryBackup() diff --git a/src/Imagekit/Models/Accounts/Origins/OriginResponse.cs b/src/Imagekit/Models/Accounts/Origins/OriginResponse.cs index b6044663..f4396642 100644 --- a/src/Imagekit/Models/Accounts/Origins/OriginResponse.cs +++ b/src/Imagekit/Models/Accounts/Origins/OriginResponse.cs @@ -149,6 +149,23 @@ public string? BaseUrlForCanonicalHeader } } + public bool? UseIamRole + { + get + { + return Match( + s3: (x) => x.UseIamRole, + s3Compatible: (_) => null, + cloudinaryBackup: (x) => x.UseIamRole, + webFolder: (_) => null, + webProxy: (_) => null, + gcs: (_) => null, + azureBlob: (_) => null, + akeneoPim: (_) => null + ); + } + } + public string? BaseUrl { get @@ -891,6 +908,28 @@ public string? BaseUrlForCanonicalHeader } } + /// + /// Whether the origin authenticates using an IAM role instead of access/secret + /// keys. + /// + public bool? UseIamRole + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNullableStruct("useIAMRole"); + } + init + { + if (value == null) + { + return; + } + + this._rawData.Set("useIAMRole", value); + } + } + /// public override void Validate() { @@ -904,6 +943,7 @@ public override void Validate() throw new ImageKitInvalidDataException("Invalid value given for constant"); } _ = this.BaseUrlForCanonicalHeader; + _ = this.UseIamRole; } public OriginResponseS3() @@ -1241,6 +1281,28 @@ public string? BaseUrlForCanonicalHeader } } + /// + /// Whether the origin authenticates using an IAM role instead of access/secret + /// keys. + /// + public bool? UseIamRole + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNullableStruct("useIAMRole"); + } + init + { + if (value == null) + { + return; + } + + this._rawData.Set("useIAMRole", value); + } + } + /// public override void Validate() { @@ -1259,6 +1321,7 @@ public override void Validate() throw new ImageKitInvalidDataException("Invalid value given for constant"); } _ = this.BaseUrlForCanonicalHeader; + _ = this.UseIamRole; } public OriginResponseCloudinaryBackup() From 9dd7498ad981888b5024e0f2f7fd161dab8e01da Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:01:29 +0000 Subject: [PATCH 3/4] docs: colorize default intensity value to 100 --- .stats.yml | 4 ++-- src/Imagekit/Models/Transformation.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9768080b..aec32876 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 48 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc/imagekit-diversion-df7d99073b94ff23f887a5bb4e9d85b09e743069e134cd62ec90967069513379.yml -openapi_spec_hash: 94d902416dd182c025a5e3840aa9c2ff +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc/imagekit-diversion-a078cd16edc143e212e23ebdb4d94a3898243c4a85e10f04811eb1ccf1b90c1a.yml +openapi_spec_hash: 4cbee56647635fe750024baf5e7dad5f config_hash: 66121ffadb78b9866f6b853f19b11f3d diff --git a/src/Imagekit/Models/Transformation.cs b/src/Imagekit/Models/Transformation.cs index bc725c60..b2d935ac 100644 --- a/src/Imagekit/Models/Transformation.cs +++ b/src/Imagekit/Models/Transformation.cs @@ -336,7 +336,7 @@ public string? Border /// Applies a color tint to the image. Accepts color and intensity as optional /// parameters. - `co-color` - Color to apply (e.g., `red`, `blue`, `FF0022`). /// Default is gray color. - `in-intensity` - Intensity of the color (0-100). - /// Default is 35. See [Colorize](https://imagekit.io/docs/effects-and-enhancements#colorize---e-colorize). + /// Default is 100. See [Colorize](https://imagekit.io/docs/effects-and-enhancements#colorize---e-colorize). /// public string? Colorize { From 2ce559bc0f230d930c25dae0b49807f24532d1f2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:01:52 +0000 Subject: [PATCH 4/4] release: 6.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 18 ++++++++++++++++++ src/Imagekit/Imagekit.csproj | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4ff26209..0e2b815d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "6.0.1" + ".": "6.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd57657..1c4b7825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 6.1.0 (2026-06-18) + +Full Changelog: [v6.0.1...v6.1.0](https://github.com/imagekit-developer/imagekit-dotnet/compare/v6.0.1...v6.1.0) + +### Features + +* **origins:** add useIAMRole for IAM role authentication ([ae25f9c](https://github.com/imagekit-developer/imagekit-dotnet/commit/ae25f9c2480ea5fec242e5f601bb6fb46d9088cf)) + + +### Bug Fixes + +* **csharp:** resolve ambiguity in parsing query strings ([7b1f3f3](https://github.com/imagekit-developer/imagekit-dotnet/commit/7b1f3f35117f35101f07ed55f173ed91a427efb9)) + + +### Documentation + +* colorize default intensity value to 100 ([9dd7498](https://github.com/imagekit-developer/imagekit-dotnet/commit/9dd7498ad981888b5024e0f2f7fd161dab8e01da)) + ## 6.0.1 (2026-05-28) Full Changelog: [v6.0.0...v6.0.1](https://github.com/imagekit-developer/imagekit-dotnet/compare/v6.0.0...v6.0.1) diff --git a/src/Imagekit/Imagekit.csproj b/src/Imagekit/Imagekit.csproj index 1c950f34..98501db0 100644 --- a/src/Imagekit/Imagekit.csproj +++ b/src/Imagekit/Imagekit.csproj @@ -3,7 +3,7 @@ Image Kit C# Imagekit - 6.0.1 + 6.1.0 The official .NET library for the Image Kit API. Library README.md