diff --git a/Elsa.sln b/Elsa.sln
index c16d086ad..f037a18c6 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -355,6 +355,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTeleme
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "secrets", "secrets", "{D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.JavaScript", "src\modules\Elsa.Secrets.JavaScript\Elsa.Secrets.JavaScript.csproj", "{80F7ED38-15BA-4C82-88C8-12135B33F7EF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1431,6 +1433,18 @@ Global
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x64.Build.0 = Release|Any CPU
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.ActiveCfg = Release|Any CPU
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.Build.0 = Release|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x64.Build.0 = Debug|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Debug|x86.Build.0 = Debug|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x64.ActiveCfg = Release|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x64.Build.0 = Release|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1558,6 +1572,7 @@ Global
{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
{D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{09B4B78B-FE02-44E2-8667-E182AF921C54} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
+ {80F7ED38-15BA-4C82-88C8-12135B33F7EF} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/src/modules/Elsa.Secrets.JavaScript/Elsa.Secrets.JavaScript.csproj b/src/modules/Elsa.Secrets.JavaScript/Elsa.Secrets.JavaScript.csproj
new file mode 100644
index 000000000..8fd62bfe0
--- /dev/null
+++ b/src/modules/Elsa.Secrets.JavaScript/Elsa.Secrets.JavaScript.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+ Provides JavaScript expression functions for resolving Elsa secrets.
+
+ elsa module secrets javascript workflow security
+
+
+
+
+
+
+
+
diff --git a/src/modules/Elsa.Secrets.JavaScript/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Secrets.JavaScript/Extensions/ModuleExtensions.cs
new file mode 100644
index 000000000..8931ef1a1
--- /dev/null
+++ b/src/modules/Elsa.Secrets.JavaScript/Extensions/ModuleExtensions.cs
@@ -0,0 +1,13 @@
+using Elsa.Features.Services;
+using Elsa.Secrets.JavaScript.Features;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+public static class ModuleExtensions
+{
+ public static IModule UseSecretsJavaScript(this IModule module, Action? configure = null)
+ {
+ return module.Use(configure);
+ }
+}
diff --git a/src/modules/Elsa.Secrets.JavaScript/Features/SecretsJavaScriptFeature.cs b/src/modules/Elsa.Secrets.JavaScript/Features/SecretsJavaScriptFeature.cs
new file mode 100644
index 000000000..811c15532
--- /dev/null
+++ b/src/modules/Elsa.Secrets.JavaScript/Features/SecretsJavaScriptFeature.cs
@@ -0,0 +1,22 @@
+using Elsa.Expressions.JavaScript.Extensions;
+using Elsa.Expressions.JavaScript.Features;
+using Elsa.Features.Abstractions;
+using Elsa.Features.Attributes;
+using Elsa.Features.Services;
+using Elsa.Secrets.Features;
+using Elsa.Secrets.JavaScript.Scripting.JavaScript;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Secrets.JavaScript.Features;
+
+[DependsOn(typeof(JavaScriptFeature))]
+[DependsOn(typeof(SecretsFeature))]
+public class SecretsJavaScriptFeature(IModule module) : FeatureBase(module)
+{
+ public override void Apply()
+ {
+ Services
+ .AddNotificationHandler()
+ .AddFunctionDefinitionProvider();
+ }
+}
diff --git a/src/modules/Elsa.Secrets.JavaScript/FodyWeavers.xml b/src/modules/Elsa.Secrets.JavaScript/FodyWeavers.xml
new file mode 100644
index 000000000..8099e287a
--- /dev/null
+++ b/src/modules/Elsa.Secrets.JavaScript/FodyWeavers.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/modules/Elsa.Secrets.JavaScript/Scripting/JavaScript/SecretsJavaScriptHandler.cs b/src/modules/Elsa.Secrets.JavaScript/Scripting/JavaScript/SecretsJavaScriptHandler.cs
new file mode 100644
index 000000000..a6f79e232
--- /dev/null
+++ b/src/modules/Elsa.Secrets.JavaScript/Scripting/JavaScript/SecretsJavaScriptHandler.cs
@@ -0,0 +1,27 @@
+using Elsa.Expressions.JavaScript.Notifications;
+using Elsa.Expressions.JavaScript.TypeDefinitions.Abstractions;
+using Elsa.Expressions.JavaScript.TypeDefinitions.Models;
+using Elsa.Mediator.Contracts;
+using Elsa.Secrets.Contracts;
+using JetBrains.Annotations;
+
+namespace Elsa.Secrets.JavaScript.Scripting.JavaScript;
+
+[UsedImplicitly]
+public class SecretsJavaScriptHandler(ISecretResolver secretResolver) : FunctionDefinitionProvider, INotificationHandler
+{
+ public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
+ {
+ var context = notification.Context;
+ notification.Engine.SetValue("getSecret", (Func>)(name => secretResolver.ResolveAsync(name, context.CancellationToken)));
+ return Task.CompletedTask;
+ }
+
+ protected override IEnumerable GetFunctionDefinitions(TypeDefinitionContext context)
+ {
+ yield return CreateFunctionDefinition(function => function
+ .Name("getSecret")
+ .ReturnType("Promise")
+ .Parameter("name", "string"));
+ }
+}
diff --git a/src/modules/Elsa.Secrets.JavaScript/ShellFeatures/SecretsJavaScriptFeature.cs b/src/modules/Elsa.Secrets.JavaScript/ShellFeatures/SecretsJavaScriptFeature.cs
new file mode 100644
index 000000000..749244b4c
--- /dev/null
+++ b/src/modules/Elsa.Secrets.JavaScript/ShellFeatures/SecretsJavaScriptFeature.cs
@@ -0,0 +1,24 @@
+using CShells.Features;
+using Elsa.Expressions.JavaScript.Extensions;
+using Elsa.Expressions.JavaScript.ShellFeatures;
+using Elsa.Secrets.JavaScript.Scripting.JavaScript;
+using Elsa.Secrets.ShellFeatures;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Secrets.JavaScript.ShellFeatures;
+
+[ShellFeature(
+ DisplayName = "Secrets JavaScript",
+ Description = "Provides JavaScript expression functions for resolving Elsa secrets.",
+ DependsOn = [typeof(JavaScriptFeature), typeof(SecretsFeature)])]
+[UsedImplicitly]
+public class SecretsJavaScriptFeature : IShellFeature
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services
+ .AddNotificationHandler()
+ .AddFunctionDefinitionProvider();
+ }
+}
diff --git a/test/integration/Elsa.JavaScript.IntegrationTests/Elsa.JavaScript.IntegrationTests.csproj b/test/integration/Elsa.JavaScript.IntegrationTests/Elsa.JavaScript.IntegrationTests.csproj
index b7f1ee64d..90ec04f35 100644
--- a/test/integration/Elsa.JavaScript.IntegrationTests/Elsa.JavaScript.IntegrationTests.csproj
+++ b/test/integration/Elsa.JavaScript.IntegrationTests/Elsa.JavaScript.IntegrationTests.csproj
@@ -8,6 +8,7 @@
+
diff --git a/test/integration/Elsa.JavaScript.IntegrationTests/SecretsJavaScriptTests.cs b/test/integration/Elsa.JavaScript.IntegrationTests/SecretsJavaScriptTests.cs
new file mode 100644
index 000000000..85563669b
--- /dev/null
+++ b/test/integration/Elsa.JavaScript.IntegrationTests/SecretsJavaScriptTests.cs
@@ -0,0 +1,108 @@
+using Elsa.Expressions.JavaScript.Contracts;
+using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts;
+using Elsa.Expressions.JavaScript.TypeDefinitions.Models;
+using Elsa.Extensions;
+using Elsa.Features.Services;
+using Elsa.Secrets.Contracts;
+using Elsa.Secrets.Models;
+using Elsa.Testing.Shared;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit.Abstractions;
+
+namespace Elsa.JavaScript.IntegrationTests;
+
+public class SecretsJavaScriptTests
+{
+ private readonly TestSecretResolver _secretResolver = new();
+ private readonly WorkflowTestFixture _fixture;
+
+ public SecretsJavaScriptTests(ITestOutputHelper testOutputHelper)
+ {
+ _fixture = new(testOutputHelper);
+ _fixture
+ .ConfigureServices(services => services.AddSingleton(_secretResolver))
+ .ConfigureElsa(ConfigureElsa);
+ }
+
+ [Fact]
+ public async Task GetSecret_ReturnsPromiseCompatibleSecretValue()
+ {
+ var result = await EvaluateScriptAsync("return getSecret('api:key');");
+
+ Assert.Equal("secret-value", result);
+ Assert.Equal(["api:key"], _secretResolver.ResolvedNames);
+ }
+
+ [Fact]
+ public async Task GetSecret_ComposesWithThen()
+ {
+ var result = await EvaluateScriptAsync("return getSecret('api:key').then(value => value + '-suffix');");
+
+ Assert.Equal("secret-value-suffix", result);
+ }
+
+ [Fact]
+ public async Task GetSecret_ComposesWithAsyncIife()
+ {
+ var result = await EvaluateScriptAsync("return (async () => await getSecret('api:key'))();");
+
+ Assert.Equal("secret-value", result);
+ }
+
+ [Fact]
+ public async Task TypeDefinitions_DocumentGetSecretAsPromiseOfString()
+ {
+ var typeDefinitions = await GenerateTypeDefinitionsAsync();
+
+ Assert.Contains("declare function getSecret(name: string): Promise;", typeDefinitions);
+ }
+
+ private static void ConfigureElsa(IModule module)
+ {
+ module.UseSecretsJavaScript();
+ }
+
+ private async Task EvaluateScriptAsync(string script)
+ {
+ var context = await _fixture.CreateExpressionExecutionContextAsync();
+ var evaluator = _fixture.Services.GetRequiredService();
+ var result = await evaluator.EvaluateAsync(script, typeof(T), context);
+
+ return result is T typedResult ? typedResult : (T)Convert.ChangeType(result, typeof(T))!;
+ }
+
+ private async Task GenerateTypeDefinitionsAsync()
+ {
+ await _fixture.BuildAsync();
+ var workflow = new Workflow();
+ var workflowGraphBuilder = _fixture.Services.GetRequiredService();
+ var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
+ var typeDefinitionContext = new TypeDefinitionContext(workflowGraph, null, null, CancellationToken.None);
+ var typeDefinitionService = _fixture.Services.GetRequiredService();
+
+ return await typeDefinitionService.GenerateTypeDefinitionsAsync(typeDefinitionContext);
+ }
+
+ private class TestSecretResolver : ISecretResolver
+ {
+ private readonly Dictionary _secrets = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["api:key"] = "secret-value"
+ };
+
+ public List ResolvedNames { get; } = [];
+
+ public Task ResolveAsync(string name, CancellationToken cancellationToken = default)
+ {
+ ResolvedNames.Add(name);
+ return Task.FromResult(_secrets[name]);
+ }
+
+ public Task ResolveAsync(SecretReference reference, CancellationToken cancellationToken = default)
+ {
+ return ResolveAsync(reference.Name, cancellationToken);
+ }
+ }
+}