Merge remote-tracking branch 'origin/codex/workflow-secret-references-s4' into codex/workflow-secret-references-s5

This commit is contained in:
Sipke Schoorstra 2026-06-01 09:38:00 +02:00
commit 4a6db8aa98
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
9 changed files with 229 additions and 0 deletions

View file

@ -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}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides JavaScript expression functions for resolving Elsa secrets.
</Description>
<PackageTags>elsa module secrets javascript workflow security</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Expressions.JavaScript\Elsa.Expressions.JavaScript.csproj" />
<ProjectReference Include="..\Elsa.Secrets\Elsa.Secrets.csproj" />
</ItemGroup>
</Project>

View file

@ -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<SecretsJavaScriptFeature>? configure = null)
{
return module.Use(configure);
}
}

View file

@ -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<SecretsJavaScriptHandler>()
.AddFunctionDefinitionProvider<SecretsJavaScriptHandler>();
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

View file

@ -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<EvaluatingJavaScript>
{
public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
{
var context = notification.Context;
notification.Engine.SetValue("getSecret", (Func<string, Task<string>>)(name => secretResolver.ResolveAsync(name, context.CancellationToken)));
return Task.CompletedTask;
}
protected override IEnumerable<FunctionDefinition> GetFunctionDefinitions(TypeDefinitionContext context)
{
yield return CreateFunctionDefinition(function => function
.Name("getSecret")
.ReturnType("Promise<string>")
.Parameter("name", "string"));
}
}

View file

@ -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<SecretsJavaScriptHandler>()
.AddFunctionDefinitionProvider<SecretsJavaScriptHandler>();
}
}

View file

@ -8,6 +8,7 @@
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Expressions.JavaScript\Elsa.Expressions.JavaScript.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Secrets.JavaScript\Elsa.Secrets.JavaScript.csproj" />
</ItemGroup>
</Project>

View file

@ -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<ISecretResolver>(_secretResolver))
.ConfigureElsa(ConfigureElsa);
}
[Fact]
public async Task GetSecret_ReturnsPromiseCompatibleSecretValue()
{
var result = await EvaluateScriptAsync<string>("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<string>("return getSecret('api:key').then(value => value + '-suffix');");
Assert.Equal("secret-value-suffix", result);
}
[Fact]
public async Task GetSecret_ComposesWithAsyncIife()
{
var result = await EvaluateScriptAsync<string>("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<string>;", typeDefinitions);
}
private static void ConfigureElsa(IModule module)
{
module.UseSecretsJavaScript();
}
private async Task<T> EvaluateScriptAsync<T>(string script)
{
var context = await _fixture.CreateExpressionExecutionContextAsync();
var evaluator = _fixture.Services.GetRequiredService<IJavaScriptEvaluator>();
var result = await evaluator.EvaluateAsync(script, typeof(T), context);
return result is T typedResult ? typedResult : (T)Convert.ChangeType(result, typeof(T))!;
}
private async Task<string> GenerateTypeDefinitionsAsync()
{
await _fixture.BuildAsync();
var workflow = new Workflow();
var workflowGraphBuilder = _fixture.Services.GetRequiredService<IWorkflowGraphBuilder>();
var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
var typeDefinitionContext = new TypeDefinitionContext(workflowGraph, null, null, CancellationToken.None);
var typeDefinitionService = _fixture.Services.GetRequiredService<ITypeDefinitionService>();
return await typeDefinitionService.GenerateTypeDefinitionsAsync(typeDefinitionContext);
}
private class TestSecretResolver : ISecretResolver
{
private readonly Dictionary<string, string> _secrets = new(StringComparer.OrdinalIgnoreCase)
{
["api:key"] = "secret-value"
};
public List<string> ResolvedNames { get; } = [];
public Task<string> ResolveAsync(string name, CancellationToken cancellationToken = default)
{
ResolvedNames.Add(name);
return Task.FromResult(_secrets[name]);
}
public Task<string> ResolveAsync(SecretReference reference, CancellationToken cancellationToken = default)
{
return ResolveAsync(reference.Name, cancellationToken);
}
}
}