diff --git a/Elsa.sln b/Elsa.sln
index 95efa28df..496846670 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -1652,17 +1652,17 @@ Global
{09B4B78B-FE02-44E2-8667-E182AF921C54} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
{7D905CEC-B30B-4C99-B5F7-3052D33EC8E9} = {18453B51-25EB-4317-A4B3-B10518252E92}
{81CFD2E0-2E5E-4810-ADB8-A08301199166} = {18453B51-25EB-4317-A4B3-B10518252E92}
- {8F4AD54E-8586-4D8C-82E6-69218DD4280F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{60F844CC-6DA6-45EB-BA5E-B480B4E77624} = {18453B51-25EB-4317-A4B3-B10518252E92}
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
{D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{80F7ED38-15BA-4C82-88C8-12135B33F7EF} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
- {0C6F614C-A698-426B-8D11-0132C2FFF400} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
- {4DEE0574-C5B3-4765-9C48-89D384A17780} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
- {151C46EF-FE3D-400B-8CBE-F86886B3EB96} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
- {E4B1546A-3F35-4B5C-815C-07E58B00163F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
- {E18931B2-82EE-4398-B38A-7B8B449957CD} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
- {729FD075-2863-48AB-A3C0-A4F7A9277263} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {0C6F614C-A698-426B-8D11-0132C2FFF400} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {E18931B2-82EE-4398-B38A-7B8B449957CD} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {729FD075-2863-48AB-A3C0-A4F7A9277263} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {E4B1546A-3F35-4B5C-815C-07E58B00163F} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {4DEE0574-C5B3-4765-9C48-89D384A17780} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {151C46EF-FE3D-400B-8CBE-F86886B3EB96} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
index 725e606b5..736a2a1d0 100644
--- a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
+++ b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
@@ -19,6 +19,7 @@
+
diff --git a/src/apps/Elsa.ModularServer.Web/appsettings.json b/src/apps/Elsa.ModularServer.Web/appsettings.json
index e14366c4f..ed66320c9 100644
--- a/src/apps/Elsa.ModularServer.Web/appsettings.json
+++ b/src/apps/Elsa.ModularServer.Web/appsettings.json
@@ -58,6 +58,7 @@
"Secrets": {
"EncryptionKey": "Q0hBTkdFX01FX1RPX0FfU0VDVVJFX1JBTkRPTV9LRVk="
},
+ "SecretsJavaScript": {},
"SqliteAlterationsPersistence": {
"ConnectionString": "Data Source=elsa_workflows.db;Cache=Shared"
},
diff --git a/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs b/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs
index 384d4515a..10f3400f4 100644
--- a/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs
+++ b/src/modules/Elsa.Secrets/Contracts/ISecretManager.cs
@@ -7,6 +7,7 @@ public interface ISecretManager
Task> ListAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
Task ListPageAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
Task CountAsync(ListSecretsRequest request, CancellationToken cancellationToken = default);
+ Task UpdateAsync(string name, UpdateSecretRequest request, CancellationToken cancellationToken = default);
Task RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default);
Task RevokeAsync(string name, CancellationToken cancellationToken = default);
Task DeleteAsync(string name, CancellationToken cancellationToken = default);
diff --git a/src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs b/src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs
new file mode 100644
index 000000000..fd26ae0c0
--- /dev/null
+++ b/src/modules/Elsa.Secrets/Endpoints/Secrets/Update/Endpoint.cs
@@ -0,0 +1,27 @@
+using Elsa.Abstractions;
+using Elsa.Secrets.Permissions;
+using Elsa.Secrets.Services;
+
+namespace Elsa.Secrets.Endpoints.Secrets.Update;
+
+internal class Endpoint(ISecretManager manager) : ElsaEndpoint
+{
+ public override void Configure()
+ {
+ Post("/secrets/{name}");
+ ConfigurePermissions(SecretsPermissions.Write);
+ }
+
+ public override async Task HandleAsync(UpdateSecretRequest request, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var secret = await manager.UpdateAsync(Route("name")!, request, cancellationToken);
+ await Send.OkAsync(secret.ToModel(), cancellationToken);
+ }
+ catch (KeyNotFoundException)
+ {
+ await Send.NotFoundAsync(cancellationToken);
+ }
+ }
+}
diff --git a/src/modules/Elsa.Secrets/Models/SecretApiModels.cs b/src/modules/Elsa.Secrets/Models/SecretApiModels.cs
index 342f4a58a..bc492797a 100644
--- a/src/modules/Elsa.Secrets/Models/SecretApiModels.cs
+++ b/src/modules/Elsa.Secrets/Models/SecretApiModels.cs
@@ -32,6 +32,12 @@ public class CreateSecretRequest
public IDictionary Metadata { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
+public class UpdateSecretRequest
+{
+ public string? DisplayName { get; set; }
+ public string? Description { get; set; }
+}
+
public class RotateSecretRequest
{
public string? Value { get; set; }
diff --git a/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs b/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs
index 8a9463f3b..b6afb4f97 100644
--- a/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs
+++ b/src/modules/Elsa.Secrets/Services/DefaultSecretManager.cs
@@ -47,6 +47,18 @@ public class DefaultSecretManager(ISecretNameValidator nameValidator, ISecretSto
return ApplyFilters(secrets, request).LongCount();
}
+ public async Task UpdateAsync(string name, UpdateSecretRequest request, CancellationToken cancellationToken = default)
+ {
+ var secret = await GetExistingAsync(name, cancellationToken);
+
+ secret.DisplayName = string.IsNullOrWhiteSpace(request.DisplayName) ? secret.Name : request.DisplayName.Trim();
+ secret.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
+ secret.UpdatedAt = DateTimeOffset.UtcNow;
+
+ await repository.SaveAsync(secret, cancellationToken);
+ return secret;
+ }
+
public async Task RotateAsync(string name, RotateSecretRequest request, CancellationToken cancellationToken = default)
{
var secret = await GetExistingAsync(name, cancellationToken);
diff --git a/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs b/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs
index 9aa78ee45..19949b273 100644
--- a/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs
+++ b/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs
@@ -173,6 +173,66 @@ public class SecretManagerTests
Assert.Equal(3, count);
}
+ [Fact]
+ public async Task UpdateAsync_UpdatesMetadataWithoutChangingIdentityOrVersion()
+ {
+ await _fixture.Manager.CreateAsync(new CreateSecretRequest
+ {
+ Name = "smtp:password",
+ DisplayName = "SMTP password",
+ Description = "Old description",
+ Scope = "production",
+ Value = "one"
+ });
+
+ var updated = await _fixture.Manager.UpdateAsync("smtp:password", new UpdateSecretRequest
+ {
+ DisplayName = " SMTP credential ",
+ Description = " Rotated manually "
+ });
+
+ Assert.Equal("smtp:password", updated.Name);
+ Assert.Equal("SMTP credential", updated.DisplayName);
+ Assert.Equal("Rotated manually", updated.Description);
+ Assert.Equal("production", updated.Scope);
+ Assert.Equal(SecretTypeNames.Text, updated.TypeName);
+ Assert.Equal(SecretStoreNames.Encrypted, updated.StoreName);
+ Assert.Single(updated.Versions);
+ Assert.NotNull(updated.UpdatedAt);
+ Assert.Equal("one", await _fixture.Resolver.ResolveAsync("smtp:password"));
+ }
+
+ [Fact]
+ public async Task UpdateAsync_UsesTechnicalNameAsFallbackDisplayName_AndClearsBlankDescription()
+ {
+ await _fixture.Manager.CreateAsync(new CreateSecretRequest
+ {
+ Name = "smtp:password",
+ DisplayName = "SMTP password",
+ Description = "Old description",
+ Value = "one"
+ });
+
+ var updated = await _fixture.Manager.UpdateAsync("smtp:password", new UpdateSecretRequest
+ {
+ DisplayName = " ",
+ Description = " "
+ });
+
+ Assert.Equal("smtp:password", updated.DisplayName);
+ Assert.Null(updated.Description);
+ }
+
+ [Fact]
+ public async Task UpdateAsync_Throws_WhenSecretDoesNotExist()
+ {
+ await Assert.ThrowsAsync(() => _fixture.Manager.UpdateAsync("missing:secret", new UpdateSecretRequest
+ {
+ DisplayName = "Missing",
+ Description = "Missing"
+ }));
+ }
+
[Fact]
public async Task ListPageAsync_AppliesFiltersBeforePaging()
{