From fb4fb63c3ab5c6e84b6e8c11efa64bb74aa79bbf Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 8 Jun 2026 10:42:59 +0200 Subject: [PATCH 01/33] Add named WithVariable overload (#7701) --- .../Elsa.Workflows.Core/Builders/WorkflowBuilder.cs | 10 +++++++++- .../Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs | 7 ++++++- .../Serialization/VariableExpressions/Workflows.cs | 4 ++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs index fd5608325..21cdef3ff 100644 --- a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs @@ -88,6 +88,14 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer return variable; } + /// + public Variable WithVariable(string name) + { + var variable = new Variable(name, default!); + Variables.Add(variable); + return variable; + } + /// public Variable WithVariable(string name, T value) { @@ -298,4 +306,4 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer await workflowDefinition.BuildAsync(this, cancellationToken); return await BuildWorkflowAsync(cancellationToken); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs index 3d7c3cb8a..971291013 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs @@ -115,6 +115,11 @@ public interface IWorkflowBuilder [Obsolete("Use the overload that takes a name instead. This overload will be removed in a future version.")] Variable WithVariable(); + /// + /// A fluent method for adding a variable to . + /// + Variable WithVariable(string name); + /// /// A fluent method for adding a variable to . /// @@ -220,4 +225,4 @@ public interface IWorkflowBuilder /// Creates a new instance using the specified definition. /// Task BuildWorkflowAsync(IWorkflow workflowDefinition, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs index 2d6cc93e0..e36d07407 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/VariableExpressions/Workflows.cs @@ -7,7 +7,7 @@ class SampleWorkflow : WorkflowBase { protected override void Build(IWorkflowBuilder workflow) { - var variable1 = workflow.WithVariable("Some variable"); + var variable1 = workflow.WithVariable("Some variable"); var variable2 = workflow.WithVariable(42); var literal1 = new Literal("Some literal"); var literal2 = new Literal(84); @@ -27,4 +27,4 @@ class SampleWorkflow : WorkflowBase } }; } -} \ No newline at end of file +} From f3ee58774292d12541cc4eea372f6b68dbd62c60 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 8 Jun 2026 12:04:55 +0200 Subject: [PATCH 02/33] [codex] Fix ForEach completion from nested flowchart (#7702) * Fix ForEach completion from nested flowchart * Assert ForEach complete output regression * Use break flag helper consistently * Cover parent flowchart ForEach completion --- .../Activities/Flowchart.Counters.cs | 9 ++- .../Behaviors/BreakBehavior.cs | 6 +- .../ForEachTests.cs | 62 ++++++++++++++++++- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs index 661b01289..b4ca36e32 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs @@ -380,10 +380,15 @@ public partial class Flowchart var flowchartContext = context.ReceiverActivityExecutionContext; await CompleteIfNoPendingWorkAsync(flowchartContext); var flowchart = (Flowchart)flowchartContext.Activity; + var canceledActivity = context.SenderActivityExecutionContext.Activity; + + if (!flowchart.Activities.Contains(canceledActivity)) + return; + var flowGraph = flowchartContext.GetFlowGraph(); var flowScope = flowchart.GetFlowScope(flowchartContext); // Propagate canceled connections visited count by scheduling with Outcomes.Empty - await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, context.SenderActivityExecutionContext.Activity, context.SenderActivityExecutionContext, Outcomes.Empty, OnChildCompletedAsync); + await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, canceledActivity, context.SenderActivityExecutionContext, Outcomes.Empty, OnChildCompletedAsync); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Core/Behaviors/BreakBehavior.cs b/src/modules/Elsa.Workflows.Core/Behaviors/BreakBehavior.cs index de5145085..c53fe871d 100644 --- a/src/modules/Elsa.Workflows.Core/Behaviors/BreakBehavior.cs +++ b/src/modules/Elsa.Workflows.Core/Behaviors/BreakBehavior.cs @@ -21,6 +21,8 @@ public class BreakBehavior : Behavior private async ValueTask OnCompleteCompositeAsync(CompleteCompositeSignal signal, SignalContext context) { + context.ReceiverActivityExecutionContext.SetIsBreaking(); + // Cancel each descendant to clear bookmarks and cancel jobs etc. await CancelDescendantsAsync(context); @@ -34,11 +36,11 @@ public class BreakBehavior : Behavior context.StopPropagation(); // Set the IsBreaking property to true. - context.ReceiverActivityExecutionContext.SetProperty("IsBreaking", true); + context.ReceiverActivityExecutionContext.SetIsBreaking(); } private async Task CancelDescendantsAsync(SignalContext context) { await context.ReceiverActivityExecutionContext.CancelActivityAsync(); } -} \ No newline at end of file +} diff --git a/test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs b/test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs index ad6858885..beda50393 100644 --- a/test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs +++ b/test/integration/Elsa.Activities.IntegrationTests/ForEachTests.cs @@ -2,7 +2,12 @@ using Elsa.Extensions; using Elsa.Testing.Shared; using Elsa.Workflows; using Elsa.Workflows.Activities; +using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Extensions; +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Management.Activities.SetOutput; using Elsa.Workflows.Models; +using Elsa.Workflows.Options; using Xunit.Abstractions; namespace Elsa.Activities.IntegrationTests; @@ -247,6 +252,61 @@ public class ForEachTests(ITestOutputHelper testOutputHelper) Assert.Equal(ActivityStatus.Running, forEachContext.Status); Assert.Equal(1, forEachContext.AggregateFaultCount); } + + [Fact(DisplayName = "ForEach completes when a nested flowchart completes the composite")] + public async Task ForEach_Completes_WhenNestedFlowchartCompletesComposite() + { + var dataSource = new[] + { + "a", "b", "c" + }; + var writeLine = WriteCurrentValue(); + var decision = new FlowDecision(context => context.GetVariable(CurrentValueVar) == "b"); + var setOutput = new SetOutput + { + OutputName = new("Output"), + OutputValue = new(context => context.GetVariable(CurrentValueVar)) + }; + var complete = new Complete(["True"]); + var forEach = new ForEach(dataSource) + { + Body = new Flowchart + { + Activities = + { + writeLine, + decision, + setOutput, + complete + }, + Connections = + { + new() { Source = new(writeLine, "Done"), Target = new(decision) }, + new() { Source = new(decision, "True"), Target = new(setOutput) }, + new() { Source = new(setOutput, "Done"), Target = new(complete) } + } + } + }; + var outerComplete = new Complete(["False"]); + var outerFlowchart = new Flowchart + { + Activities = + { + forEach, + outerComplete + }, + Connections = + { + new() { Source = new(forEach, "Done"), Target = new(outerComplete) } + } + }; + var options = new RunWorkflowOptions().WithCounterBasedFlowchart(); + + var result = await _fixture.RunActivityAsync(outerFlowchart, options); + + Assert.Equal(new[] { "a", "b" }, _fixture.CapturingTextWriter.Lines); + Assert.Equal("b", result.WorkflowState.Output["Output"]); + } private static WriteLine WriteCurrentValue() => new(context => context.GetVariable(CurrentValueVar)); @@ -260,4 +320,4 @@ public class ForEachTests(ITestOutputHelper testOutputHelper) record Foo(string Bar = "Baz") { public override string ToString() => Bar; -} \ No newline at end of file +} From 1d1c68282c19f960a821651c7c9404434f12e473 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 9 Jun 2026 06:09:47 +0200 Subject: [PATCH 03/33] fix: use tenant-agnostic application lookup for api keys --- .../ApplicationProviderExtensions.cs | 8 ++-- .../Elsa.Identity/Models/ApplicationFilter.cs | 7 +++- .../DefaultApplicationCredentialsValidator.cs | 2 +- .../Modules/Identity/ApplicationStore.cs | 4 +- .../Services/DefaultSecretHasherTests.cs | 41 +++++++++++++++++++ 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs b/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs index 5de5515cf..a99a7e3cf 100644 --- a/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs +++ b/src/modules/Elsa.Identity/Extensions/ApplicationProviderExtensions.cs @@ -15,15 +15,17 @@ public static class ApplicationProviderExtensions /// /// The user provider. /// The client ID to search by. + /// Whether to bypass tenant scoping when resolving the application. /// The cancellation token. /// The application with the specified client ID. - public static async Task FindByClientIdAsync(this IApplicationProvider applicationProvider, string clientId, CancellationToken cancellationToken = default) + public static async Task FindByClientIdAsync(this IApplicationProvider applicationProvider, string clientId, bool tenantAgnostic = false, CancellationToken cancellationToken = default) { var filter = new ApplicationFilter { - ClientId = clientId + ClientId = clientId, + TenantAgnostic = tenantAgnostic }; return await applicationProvider.FindAsync(filter, cancellationToken); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Identity/Models/ApplicationFilter.cs b/src/modules/Elsa.Identity/Models/ApplicationFilter.cs index 9de2b4ad4..c19d7c5f2 100644 --- a/src/modules/Elsa.Identity/Models/ApplicationFilter.cs +++ b/src/modules/Elsa.Identity/Models/ApplicationFilter.cs @@ -21,6 +21,11 @@ public class ApplicationFilter /// Gets or sets the application name to filter for. /// public string? Name { get; set; } + + /// + /// Gets or sets a value indicating whether to ignore tenant scoping when querying the application store. + /// + public bool TenantAgnostic { get; set; } /// /// Applies the filter to the specified queryable. @@ -36,4 +41,4 @@ public class ApplicationFilter return queryable; } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs b/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs index 6ddfe646f..2c19e2198 100644 --- a/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs +++ b/src/modules/Elsa.Identity/Services/DefaultApplicationCredentialsValidator.cs @@ -50,7 +50,7 @@ public class DefaultApplicationCredentialsValidator : IApplicationCredentialsVal return null; var clientId = _apiKeyParser.Parse(apiKey); - var application = await _applicationProvider.FindByClientIdAsync(clientId, cancellationToken); + var application = await _applicationProvider.FindByClientIdAsync(clientId, tenantAgnostic: true, cancellationToken); if(application == null) return null; diff --git a/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs b/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs index 48732d8ac..69dbaf2ab 100644 --- a/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs +++ b/src/modules/Elsa.Persistence.EFCore/Modules/Identity/ApplicationStore.cs @@ -34,8 +34,8 @@ public class EFCoreApplicationStore : IApplicationStore /// public async Task FindAsync(ApplicationFilter filter, CancellationToken cancellationToken = default) { - return await _applicationStore.FindAsync(query => Filter(query, filter), cancellationToken); + return await _applicationStore.FindAsync(query => Filter(query, filter), filter.TenantAgnostic, cancellationToken); } private static IQueryable Filter(IQueryable query, ApplicationFilter filter) => filter.Apply(query); -} \ No newline at end of file +} diff --git a/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs b/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs index d47e04d93..c343b3441 100644 --- a/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs +++ b/test/unit/Elsa.Identity.UnitTests/Services/DefaultSecretHasherTests.cs @@ -265,6 +265,34 @@ public class DefaultSecretHasherTests Assert.Equal(encodedLegacyHash, application.HashedApiKey); } + [Fact] + public async Task ValidateAsync_UsesTenantAgnosticLookupForApplications() + { + var apiKeyGenerator = new DefaultApiKeyGeneratorAndParser(); + var apiKey = apiKeyGenerator.Generate("client-1"); + var hasher = new DefaultSecretHasher(); + var hashedApiKey = hasher.HashSecret(apiKey); + var application = new Application + { + Id = "app-1", + ClientId = "client-1", + Name = "Client 1", + HashedApiKey = hashedApiKey.EncodeSecret(), + HashedApiKeySalt = hashedApiKey.EncodeSalt(), + HashedClientSecret = "", + HashedClientSecretSalt = "" + }; + var applicationProvider = new RecordingApplicationProvider(application); + var validator = new DefaultApplicationCredentialsValidator(apiKeyGenerator, applicationProvider, hasher); + + var validatedApplication = await validator.ValidateAsync(apiKey); + + Assert.Same(application, validatedApplication); + Assert.NotNull(applicationProvider.LastFilter); + Assert.True(applicationProvider.LastFilter!.TenantAgnostic); + Assert.Equal("client-1", applicationProvider.LastFilter.ClientId); + } + private static HashedSecret CreateLegacyHash(string secret) { var salt = RandomNumberGenerator.GetBytes(32); @@ -370,6 +398,19 @@ public class DefaultSecretHasherTests } } + private sealed class RecordingApplicationProvider(Application application) : IApplicationProvider + { + private readonly Application _application = application; + public ApplicationFilter? LastFilter { get; private set; } + + public Task FindAsync(ApplicationFilter filter, CancellationToken cancellationToken = default) + { + LastFilter = filter; + var application = filter.Apply(new[] { _application }.AsQueryable()).FirstOrDefault(); + return Task.FromResult(application); + } + } + private sealed class CultureScope : IDisposable { private readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture; From a5a3aa77ae6b6a4f0229f9157626a84ebaa25d55 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 9 Jun 2026 21:28:27 +0200 Subject: [PATCH 04/33] Guard missing NotFoundActivity descriptor during deserialization --- .../Serialization/Converters/ActivityJsonConverter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs index 29da39c2f..46627c507 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs @@ -42,7 +42,11 @@ public class ActivityJsonConverter( // If the activity type is not found, create a NotFoundActivity instead. if (activityDescriptor == null) { - var notFoundActivityDescriptor = activityRegistry.Find()!; + var notFoundActivityDescriptor = activityRegistry.Find(); + + if (notFoundActivityDescriptor == null) + throw new InvalidOperationException($"Unable to deserialize activity type '{activityTypeName}' because the NotFoundActivity descriptor is not registered. Ensure the activity registry has been populated before deserializing workflows."); + var notFoundActivityResult = JsonActivityConstructorContextHelper.CreateActivity(notFoundActivityDescriptor, activityRoot, clonedOptions); LogExceptionsIfAny(notFoundActivityResult); From 42ef838323d95b75f5b444f241be8f862f1fdd92 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 9 Jun 2026 21:28:29 +0200 Subject: [PATCH 05/33] Add regression test for missing NotFoundActivity descriptor --- .../Converters/ActivityJsonConverterTests.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs index 7782aeb13..9ce562df4 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs @@ -48,11 +48,7 @@ public sealed class ActivityJsonConverterTests public void When_DeserializeUnknownActivity_Then_ReturnsNotFoundActivity() { // Arrange - var activityRegistry = Substitute.For(); - activityRegistry - .Find(NotFoundActivityTypeName) - .Returns(new ActivityDescriptor()); - + var activityRegistry = CreateUnknownActivityRegistry(new ActivityDescriptor()); var sut = CreateSut(activityRegistry); // Act @@ -71,6 +67,20 @@ public sealed class ActivityJsonConverterTests Assert.True(notFoundActivity.Metadata.ContainsKey("description")); } + [Fact] + public void When_DeserializeUnknownActivity_And_NotFoundActivityDescriptorMissing_Then_ThrowsClearException() + { + // Arrange + var activityRegistry = CreateUnknownActivityRegistry(); + var sut = CreateSut(activityRegistry); + + // Act + var exception = Assert.Throws(() => Execute(sut, UnknownActivityJson)); + + // Assert + Assert.Equal($"Unable to deserialize activity type '{UnknownActivityTypeName}' because the NotFoundActivity descriptor is not registered. Ensure the activity registry has been populated before deserializing workflows.", exception.Message); + } + [Fact] public void When_DeserializeWorkflowAsActivity_And_WorkflowDefinitionIdSpecified_Then_FindsAndInstantiatesActivity() { @@ -136,6 +146,13 @@ public sealed class ActivityJsonConverterTests return activityRegistry; } + static IActivityRegistry CreateUnknownActivityRegistry(ActivityDescriptor? notFoundActivityDescriptor = null) + { + var activityRegistry = Substitute.For(); + activityRegistry.Find(NotFoundActivityTypeName).Returns(notFoundActivityDescriptor); + return activityRegistry; + } + static IActivityRegistry CreateActivityRegistry_FindByCustomProperty(string customPropertyName, string customPropertyValue) { var descriptor = new ActivityDescriptor @@ -352,4 +369,4 @@ public sealed class ActivityJsonConverterTests } private static ILogger CreateLogger() => LoggerFactory.Create(_ => { }).CreateLogger(); -} +} \ No newline at end of file From ded765b379e60402492cb228e8bb88ce2f6e0986 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 10 Jun 2026 02:05:50 +0200 Subject: [PATCH 06/33] docs: refresh roadmap --- ROADMAP.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4470b613d..385dde1f9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Elsa Roadmap -Last refreshed: 2026-05-29 +Last refreshed: 2026-06-10 This roadmap is a product direction document, not a fixed release calendar. Elsa is developed through a mix of core maintainer work, customer-funded work, and community contributions, so sequencing can change when real-world demand changes. The intent is stable: make Elsa the most productive, dependable, and extensible workflow platform for the .NET ecosystem. @@ -39,7 +39,7 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio - [x] Structured logs - [x] Console logs - [x] Studio structured-log, console-log, and OpenTelemetry diagnostics foundations -- [~] Durable structured log persistence +- [x] Durable structured log persistence - [~] Scheduler and message-bus foundations through Quartz, Hangfire, MassTransit, Kafka, and Azure Service Bus - [~] OpenTelemetry diagnostics backend and default workflow metrics - [ ] Scheduler/message reliability hardening for clustered production workloads @@ -93,7 +93,7 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio These are already present in the codebase and should be treated as foundations for the next roadmap slices: - Multi-targeting for `net8.0`, `net9.0`, and `net10.0` in [`src/Directory.Build.props`](src/Directory.Build.props). -- The `3.7.0` release train shipped across [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0), [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.0), and [Extensions](https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.0) in May 2026, promoting shell integration, Studio authentication, workflow diagnostics, and extension package metadata into released foundations. +- The `3.7.0` release train shipped across [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0), [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.0), and [Extensions](https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.0) in May 2026, promoting shell integration, Studio authentication, workflow diagnostics, and extension package metadata into released foundations. The [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.8.0-preview1) and [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.8.0-preview1) `3.8.0-preview1` releases on June 1, 2026 then added the next preview slice of graceful shutdown, richer diagnostics, secrets, and newer designer surfaces. - Modular core packages under [`src/modules`](src/modules), with code-first features and CShells shell features documented in [`doc/wiki/module-system.md`](doc/wiki/module-system.md). - A modular server host using CShells and Nuplane package loading in [`src/apps/Elsa.ModularServer.Web`](src/apps/Elsa.ModularServer.Web). - Runtime admin, quiescence, drain, and interrupted recovery infrastructure in [`Elsa.Workflows.Runtime`](src/modules/Elsa.Workflows.Runtime) and runtime admin endpoints in [`Elsa.Workflows.Api`](src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin). @@ -101,13 +101,14 @@ These are already present in the codebase and should be treated as foundations f - Structured diagnostics with recent/live capture plus SQLite persistence in [`Elsa.Diagnostics.StructuredLogs`](src/modules/Elsa.Diagnostics.StructuredLogs) and [`Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite`](src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite). - Raw stdout/stderr console diagnostics in [`Elsa.Diagnostics.ConsoleLogs`](src/modules/Elsa.Diagnostics.ConsoleLogs), with the post-3.7 console pipeline now carrying workflow and activity execution context through [PR #7536](https://github.com/elsa-workflows/elsa-core/pull/7536). - Core OpenTelemetry diagnostics are actively in productization through [PR #7537](https://github.com/elsa-workflows/elsa-core/pull/7537), which adds OTLP ingestion, bounded storage, REST APIs, SignalR live updates, collector configuration, security checks, and tests. +- Core `main` now includes `Elsa.AI.Abstractions`, `Elsa.AI.Host`, `Elsa.AI.Copilot`, and `Elsa.AI.Persistence.EFCore` through [PR #7523](https://github.com/elsa-workflows/elsa-core/pull/7523), giving Weaver a merged server-side foundation while Studio UX and broader productization remain roadmap work. - State machine core activity support in [`Elsa.Workflows.Core/Activities/StateMachine`](src/modules/Elsa.Workflows.Core/Activities/StateMachine). - ElsaScript DSL and blob storage integration in [`Elsa.Dsl.ElsaScript`](src/modules/Elsa.Dsl.ElsaScript) and [`Elsa.WorkflowProviders.BlobStorage.ElsaScript`](src/modules/Elsa.WorkflowProviders.BlobStorage.ElsaScript). - Activity unit testing helpers and guidance in [`src/common/Elsa.Testing.Shared`](src/common/Elsa.Testing.Shared) and [`doc/qa/test-guidelines.md`](doc/qa/test-guidelines.md). - Label infrastructure in [`Elsa.Labels`](src/modules/Elsa.Labels), which is the likely backend foundation for workflow categories, tags, and folders. - Elsa Studio is already a modular Blazor product shell with workflow authoring, instance browsing, designer modules, diagnostics, authentication, localization, branding, custom elements, and early React wrapper work in [elsa-workflows/elsa-studio](https://github.com/elsa-workflows/elsa-studio). - Studio `3.7.0` shipped the modern authentication framework, Elsa Identity and OIDC modules, activity call-stack visualization, incident count badges, pending-instance filtering, and custom theme/DataPanel extensibility. -- Studio `main` includes structured-log, console-log, and OpenTelemetry diagnostics modules, newer React Flow/sequence/state-machine designer work, OIDC/identity infrastructure, custom elements, and an alterations module. The OpenTelemetry Studio module landed in [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834). +- Studio `3.8.0-preview1` shipped the server logs module, console logs module, structured-log storage diagnostics, the OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), sequence and state-machine designer foundations, the secrets module, and the alterations designer. - Elsa Extensions is an active modular integration repository with 70+ module projects in [elsa-workflows/elsa-extensions](https://github.com/elsa-workflows/elsa-extensions), targeting `net8.0`, `net9.0`, and `net10.0`. - Extensions already provide broad integration foundations: Connections, Secrets, Agents, OpenAPI, SQL/CSV/data tooling, messaging, schedulers, cloud storage, logging, webhooks, persistence providers, and external system activities. - Extensions `3.7.0` adds package manifest metadata, infrastructure attributes, shell features for MassTransit/Quartz/Webhooks, Dapper and MongoDB activity execution-chain lookups, Dapper bookmark queue filtering, Kafka multitenancy/schema-trigger work, Quartz lifecycle/job cleanup fixes, and other operational hardening. @@ -193,7 +194,7 @@ Recommended success measures: High-value items: -- Finish the diagnostics trilogy: structured logs, console logs, and OpenTelemetry. Structured and console logs now exist; Studio `main` has an OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), and Core has an active backend PR for OTLP ingestion, bounded stores, REST endpoints, SignalR live updates, collector configuration, and tests in [#7537](https://github.com/elsa-workflows/elsa-core/pull/7537). The remaining product work is to merge, release, document, and correlate this with workflow incidents. +- Finish the diagnostics trilogy: structured logs, console logs, and OpenTelemetry. Structured and console logs now exist; Studio `3.8.0-preview1` ships an OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), and Core still has an active backend PR for OTLP ingestion, bounded stores, REST endpoints, SignalR live updates, collector configuration, and tests in [#7537](https://github.com/elsa-workflows/elsa-core/pull/7537). The remaining product work is to merge and release the Core backend, document collector setup, and correlate this with workflow incidents. - Add default workflow semantic metrics: started, resumed, suspended, faulted, completed, active, activity executed/faulted, queue depth, recovery count, drain count, and dispatch latency. [#5988](https://github.com/elsa-workflows/elsa-core/issues/5988) remains the durable demand signal, while [#7537](https://github.com/elsa-workflows/elsa-core/pull/7537) supplies the first current module boundary. - Build Studio diagnostics pages that are useful under pressure: live console, structured logs, OpenTelemetry traces/metrics/logs, workflow incident timelines, source health, dropped-event counters, source selection, filters, URL state, export/copy affordances, and direct deep links to workflow instances. - Make execution history easier to reason about: distinguish faulted, interrupted, cancelled, crash-recovered, retried, and operator-modified workflows consistently across API, Studio, logs, and metrics. @@ -231,10 +232,10 @@ Recommended success measures: High-value items: -- Build AI-assisted workflow generation that produces multiple visible activities from intent rather than hiding logic in one script activity. This direction is proposed in [discussion #7367](https://github.com/elsa-workflows/elsa-core/discussions/7367), and [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) now provides an active Weaver AI Copilot foundation with AI abstractions, provider/session contracts, chat/tool endpoints, audit events, proposal persistence, EF Core storage, and integration/unit tests. +- Build AI-assisted workflow generation that produces multiple visible activities from intent rather than hiding logic in one script activity. This direction is proposed in [discussion #7367](https://github.com/elsa-workflows/elsa-core/discussions/7367), and merged [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) now provides the first Weaver AI Copilot server foundation with AI abstractions, provider/session contracts, chat/tool endpoints, audit events, proposal persistence, EF Core storage, and integration/unit tests. - Provide an Elsa MCP/tooling surface for reading, validating, editing, and explaining workflow JSON/ElsaScript. This would make Elsa a strong fit for AI-enabled .NET development environments. - Align AI authoring with the Extensions Agents work: provider abstractions, MCP tools, OpenAI/Claude/local model support, tool approval, secrets handling, and Studio UX should share contracts instead of creating parallel AI stacks. -- Build a Studio copilot only after the authoring contracts are stable: validation, generated activity metadata, designer APIs, diagnostics links, and test scaffolding should be available before AI generation becomes prominent. [elsa-studio#553](https://github.com/elsa-workflows/elsa-studio/issues/553) has clear community signal and maintainer interest, while [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) is still Core/backend-oriented and should not be treated as a complete Studio product surface. +- Build a Studio copilot only after the authoring contracts are stable: validation, generated activity metadata, designer APIs, diagnostics links, and test scaffolding should be available before AI generation becomes prominent. [elsa-studio#553](https://github.com/elsa-workflows/elsa-studio/issues/553) has clear community signal and maintainer interest, while merged [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) is still Core/backend-oriented and should not be treated as a complete Studio product surface. - Add "explain this workflow", "find risky activities", "suggest tests", and "generate migration notes" capabilities backed by workflow graph metadata. - Pair AI generation with validation: generated workflows should include test scaffolds, required input/output definitions, secrets handling, and clear review diffs. From 9c24f5efe5b571f334fbd68f78fbd151e8a7b9f9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 11 Jun 2026 20:33:10 +0200 Subject: [PATCH 07/33] Dispose parsed activity JsonDocuments (#7713) * Dispose parsed activity JsonDocuments * Add ActivityJsonConverter disposal regression test * Fix ActivityJsonConverter disposal regression test file * address greptile test coverage feedback --- .../Converters/ActivityJsonConverter.cs | 101 ++++++++++-------- .../Converters/ActivityJsonConverterTests.cs | 48 ++++++++- 2 files changed, 101 insertions(+), 48 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs index 29da39c2f..abd313c05 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs @@ -27,57 +27,70 @@ public class ActivityJsonConverter( if (!JsonDocument.TryParseValue(ref reader, out var doc)) throw new JsonException("Failed to parse JsonDocument"); - var activityRoot = doc.RootElement; - var activityTypeName = GetActivityDetails(activityRoot, out var activityTypeVersion, out var activityDescriptor); - var notFoundActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(); - - // If the activity type is a NotFoundActivity, try to extract the original activity type name and version. - if (activityTypeName.Equals(notFoundActivityTypeName) && activityRoot.TryGetProperty("originalActivityJson", out var originalActivityJson)) + using (doc) { - activityRoot = JsonDocument.Parse(originalActivityJson.GetString()!).RootElement; - activityTypeName = GetActivityDetails(activityRoot, out activityTypeVersion, out activityDescriptor); - } + JsonDocument? originalActivityDoc = null; - var clonedOptions = GetClonedOptions(options); - // If the activity type is not found, create a NotFoundActivity instead. - if (activityDescriptor == null) - { - var notFoundActivityDescriptor = activityRegistry.Find()!; - var notFoundActivityResult = JsonActivityConstructorContextHelper.CreateActivity(notFoundActivityDescriptor, activityRoot, clonedOptions); - LogExceptionsIfAny(notFoundActivityResult); - - var notFoundActivity = notFoundActivityResult.Activity; - notFoundActivity.Type = notFoundActivityTypeName; - notFoundActivity.Version = 1; - notFoundActivity.MissingTypeName = activityTypeName; - notFoundActivity.MissingTypeVersion = activityTypeVersion; - notFoundActivity.OriginalActivityJson = activityRoot.ToString(); - - // Extract metadata from doc.RootElement rather than activityRoot. - // In round-trip scenarios, activityRoot may have been reassigned to the inner originalActivityJson (see line 37), - // but we want the metadata from the current activity being deserialized, which represents the NotFoundActivity - // placeholder's position and annotations in the designer. - if (doc.RootElement.TryGetProperty("metadata", out var outerMetadataElement)) + try { - var outerMetadata = JsonSerializer.Deserialize>(outerMetadataElement.GetRawText(), clonedOptions); - if (outerMetadata != null) + var activityRoot = doc.RootElement; + var activityTypeName = GetActivityDetails(activityRoot, out var activityTypeVersion, out var activityDescriptor); + var notFoundActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(); + + // If the activity type is a NotFoundActivity, try to extract the original activity type name and version. + if (activityTypeName.Equals(notFoundActivityTypeName) && activityRoot.TryGetProperty("originalActivityJson", out var originalActivityJson)) { - notFoundActivity.Metadata = outerMetadata; + originalActivityDoc = JsonDocument.Parse(originalActivityJson.GetString()!); + activityRoot = originalActivityDoc.RootElement; + activityTypeName = GetActivityDetails(activityRoot, out activityTypeVersion, out activityDescriptor); } + + var clonedOptions = GetClonedOptions(options); + // If the activity type is not found, create a NotFoundActivity instead. + if (activityDescriptor == null) + { + var notFoundActivityDescriptor = activityRegistry.Find()!; + var notFoundActivityResult = JsonActivityConstructorContextHelper.CreateActivity(notFoundActivityDescriptor, activityRoot, clonedOptions); + LogExceptionsIfAny(notFoundActivityResult); + + var notFoundActivity = notFoundActivityResult.Activity; + notFoundActivity.Type = notFoundActivityTypeName; + notFoundActivity.Version = 1; + notFoundActivity.MissingTypeName = activityTypeName; + notFoundActivity.MissingTypeVersion = activityTypeVersion; + notFoundActivity.OriginalActivityJson = activityRoot.ToString(); + + // Extract metadata from doc.RootElement rather than activityRoot. + // In round-trip scenarios, activityRoot may have been reassigned to the inner originalActivityJson (see line 42), + // but we want the metadata from the current activity being deserialized, which represents the NotFoundActivity + // placeholder's position and annotations in the designer. + if (doc.RootElement.TryGetProperty("metadata", out var outerMetadataElement)) + { + var outerMetadata = JsonSerializer.Deserialize>(outerMetadataElement.GetRawText(), clonedOptions); + if (outerMetadata != null) + { + notFoundActivity.Metadata = outerMetadata; + } + } + + // Set display text and description after metadata assignment to ensure they always reflect the current state + notFoundActivity.SetDisplayText($"Not Found: {activityTypeName}"); + notFoundActivity.SetDescription($"Could not find activity type {activityTypeName} with version {activityTypeVersion}"); + + return notFoundActivity; + } + + var context = JsonActivityConstructorContextHelper.Create(activityDescriptor, activityRoot, clonedOptions); + var activityResult = activityDescriptor.Constructor(context); + LogExceptionsIfAny(activityResult); + + return activityResult.Activity; + } + finally + { + originalActivityDoc?.Dispose(); } - - // Set display text and description after metadata assignment to ensure they always reflect the current state - notFoundActivity.SetDisplayText($"Not Found: {activityTypeName}"); - notFoundActivity.SetDescription($"Could not find activity type {activityTypeName} with version {activityTypeVersion}"); - - return notFoundActivity; } - - var context = JsonActivityConstructorContextHelper.Create(activityDescriptor, activityRoot, clonedOptions); - var activityResult = activityDescriptor.Constructor(context); - LogExceptionsIfAny(activityResult); - - return activityResult.Activity; } void LogExceptionsIfAny(ActivityConstructionResult result) diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs index 7782aeb13..5d088a57b 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Elsa.Common.Serialization; using Elsa.Expressions.Services; using Elsa.Workflows.Activities; @@ -63,10 +63,31 @@ public sealed class ActivityJsonConverterTests var notFoundActivity = (NotFoundActivity)result; Assert.Equal(UnknownActivityTypeName, notFoundActivity.MissingTypeName); Assert.Equal(0, notFoundActivity.MissingTypeVersion); + AssertEquivalentJson(UnknownActivityJson, notFoundActivity.OriginalActivityJson); + Assert.True(notFoundActivity.Metadata.ContainsKey("displayText")); + Assert.True(notFoundActivity.Metadata.ContainsKey("description")); + } - var expectedJsonDoc = JsonDocument.Parse(UnknownActivityJson); - var actualJsonDoc = JsonDocument.Parse(notFoundActivity.OriginalActivityJson); - Assert.Equal(expectedJsonDoc.RootElement.ToString(), actualJsonDoc.RootElement.ToString()); + [Fact] + public void When_DeserializeNestedNotFoundActivity_Then_PreservesMissingActivityBehavior() + { + // Arrange + var activityRegistry = Substitute.For(); + activityRegistry + .Find(NotFoundActivityTypeName) + .Returns(new ActivityDescriptor()); + + var sut = CreateSut(activityRegistry); + + // Act + var result = Execute(sut, NestedNotFoundActivityJson); + + // Assert + var notFoundActivity = Assert.IsType(result); + Assert.Equal(UnknownActivityTypeName, notFoundActivity.MissingTypeName); + Assert.Equal(0, notFoundActivity.MissingTypeVersion); + AssertEquivalentJson(UnknownActivityJson, notFoundActivity.OriginalActivityJson); + Assert.True(notFoundActivity.Metadata.ContainsKey("outerMarker")); Assert.True(notFoundActivity.Metadata.ContainsKey("displayText")); Assert.True(notFoundActivity.Metadata.ContainsKey("description")); } @@ -123,6 +144,13 @@ public sealed class ActivityJsonConverterTests static IActivity? Execute(ActivityJsonConverter sut, string json) => JsonSerializer.Deserialize(json, GetSerializerOptions(sut)); + static void AssertEquivalentJson(string expectedJson, string actualJson) + { + using var expectedJsonDoc = JsonDocument.Parse(expectedJson); + using var actualJsonDoc = JsonDocument.Parse(actualJson); + Assert.Equal(expectedJsonDoc.RootElement.ToString(), actualJsonDoc.RootElement.ToString()); + } + static IActivityRegistry CreateActivityRegistry(string typeName, IActivity activity, int? version = null) { var descriptor = new ActivityDescriptor { Constructor = _ => new(activity) }; @@ -177,6 +205,18 @@ public sealed class ActivityJsonConverterTests private static readonly WriteLine WriteLineActivity = new("Hello world!"); private static readonly string WriteLineActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(); private static readonly string NotFoundActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(); + private static readonly string NestedNotFoundActivityJson = + $$""" + { + "id": "wrapped-not-found", + "type": "{{NotFoundActivityTypeName}}", + "version": 1, + "originalActivityJson": {{JsonSerializer.Serialize(UnknownActivityJson)}}, + "metadata": { + "outerMarker": "preserved" + } + } + """; private const string WriteLineActivityJson_WithVersion = """ From 48a087e71e2b2d8c2f5b01bd9d3e5b80226383eb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Jun 2026 19:02:23 +0200 Subject: [PATCH 08/33] Restore RequestAborted after timed HTTP workflow failures (#7712) * Fix RequestAborted restoration for timed HTTP workflows * Add timeout restoration tests for HttpWorkflowsMiddleware --- .../Middleware/HttpWorkflowsMiddleware.cs | 16 ++--- .../HttpWorkflowsMiddlewareTests.cs | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs index 53d529d4c..577a062ff 100644 --- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs +++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs @@ -252,13 +252,15 @@ public class HttpWorkflowsMiddleware(RequestDelegate next) // Replace the original cancellation token with the combined one. httpContext.RequestAborted = combinedTokenSource.Token; - // Execute the action. - var result = await action(httpContext.RequestAborted); - - // Restore the original cancellation token. - httpContext.RequestAborted = originalCancellationToken; - - return result; + try + { + return await action(httpContext.RequestAborted); + } + finally + { + // Restore the original cancellation token even when execution faults or is canceled. + httpContext.RequestAborted = originalCancellationToken; + } } private HttpRouteData GetMatchingRoute(IServiceProvider serviceProvider, string path) diff --git a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs index 529b3c226..90eac8bed 100644 --- a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs +++ b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Reflection; using Elsa.Http.Bookmarks; using Elsa.Http.Middleware; using Elsa.Http.Options; @@ -14,6 +15,7 @@ namespace Elsa.Http.UnitTests.Middleware; public class HttpWorkflowsMiddlewareTests { + private static readonly MethodInfo ExecuteWithinTimeoutAsyncMethod = typeof(HttpWorkflowsMiddleware).GetMethod("ExecuteWithinTimeoutAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; private const string CurrentTenantId = "tenant-a"; private const string OtherTenantId = "tenant-b"; private const string BookmarkHash = "http-endpoint:/colliding:get"; @@ -54,6 +56,63 @@ public class HttpWorkflowsMiddlewareTests Assert.False(filter.TenantAgnostic); } + [Fact] + public async Task ExecuteWithinTimeoutAsync_RestoresRequestAbortedAfterSuccess() + { + using var requestAbortedSource = new CancellationTokenSource(); + var httpContext = new DefaultHttpContext { RequestAborted = requestAbortedSource.Token }; + var observedToken = CancellationToken.None; + + var result = await ExecuteWithinTimeoutAsync(async cancellationToken => + { + observedToken = cancellationToken; + Assert.Equal(cancellationToken, httpContext.RequestAborted); + await Task.CompletedTask; + return 42; + }, TimeSpan.FromSeconds(1), httpContext); + + Assert.Equal(42, result); + Assert.NotEqual(requestAbortedSource.Token, observedToken); + Assert.Equal(requestAbortedSource.Token, httpContext.RequestAborted); + } + + [Fact] + public async Task ExecuteWithinTimeoutAsync_RestoresRequestAbortedAfterFault() + { + using var requestAbortedSource = new CancellationTokenSource(); + var httpContext = new DefaultHttpContext { RequestAborted = requestAbortedSource.Token }; + + await Assert.ThrowsAsync(() => ExecuteWithinTimeoutAsync(_ => throw new InvalidOperationException("Boom"), TimeSpan.FromSeconds(1), httpContext)); + + Assert.Equal(requestAbortedSource.Token, httpContext.RequestAborted); + } + + [Fact] + public async Task ExecuteWithinTimeoutAsync_RestoresRequestAbortedAfterCancellation() + { + using var requestAbortedSource = new CancellationTokenSource(); + requestAbortedSource.Cancel(); + + var httpContext = new DefaultHttpContext { RequestAborted = requestAbortedSource.Token }; + var observedToken = CancellationToken.None; + + await Assert.ThrowsAnyAsync(() => ExecuteWithinTimeoutAsync(cancellationToken => + { + observedToken = cancellationToken; + return Task.FromCanceled(cancellationToken); + }, TimeSpan.FromSeconds(1), httpContext)); + + Assert.True(observedToken.IsCancellationRequested); + Assert.Equal(requestAbortedSource.Token, httpContext.RequestAborted); + } + + private async Task ExecuteWithinTimeoutAsync(Func> action, TimeSpan? requestTimeout, HttpContext httpContext) + { + var method = ExecuteWithinTimeoutAsyncMethod.MakeGenericMethod(typeof(T)); + var task = (Task)method.Invoke(_middleware, [action, requestTimeout, httpContext])!; + return await task; + } + private static IEnumerable CreateCollidingHttpEndpointBookmarks() { yield return CreateBookmark("current-tenant-bookmark", CurrentTenantId); From 0d7c8106887c43db0717f3f2eab740f2c023e174 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Jun 2026 19:07:15 +0200 Subject: [PATCH 09/33] address greptile review feedback --- .../Serialization/Converters/ActivityJsonConverterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs index 9ce562df4..08e8ce0ec 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs @@ -369,4 +369,4 @@ public sealed class ActivityJsonConverterTests } private static ILogger CreateLogger() => LoggerFactory.Create(_ => { }).CreateLogger(); -} \ No newline at end of file +} From 8f721e1ea2038c48eb8600fc97e4451c287fab7d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 14 Jun 2026 22:23:38 +0200 Subject: [PATCH 10/33] Guard HTTP fault handling when workflow reload returns null (#7714) * Guard HTTP fault handling when workflow reload returns null * Add HTTP fault handler reload guard tests * Address Greptile review feedback --- .../Middleware/HttpWorkflowsMiddleware.cs | 4 +- .../HttpWorkflowsMiddlewareTests.cs | 78 ++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs index 577a062ff..019a169c9 100644 --- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs +++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs @@ -361,8 +361,8 @@ public class HttpWorkflowsMiddleware(RequestDelegate next) var httpEndpointFaultHandler = serviceProvider.GetRequiredService(); var workflowInstanceManager = serviceProvider.GetRequiredService(); - var workflowState = (await workflowInstanceManager.FindByIdAsync(workflowExecutionResult.WorkflowState.Id, cancellationToken))!; - await httpEndpointFaultHandler.HandleAsync(new(httpContext, workflowState.WorkflowState, cancellationToken)); + var workflowState = (await workflowInstanceManager.FindByIdAsync(workflowExecutionResult.WorkflowState.Id, cancellationToken))?.WorkflowState ?? workflowExecutionResult.WorkflowState; + await httpEndpointFaultHandler.HandleAsync(new(httpContext, workflowState, cancellationToken)); return true; } diff --git a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs index 90eac8bed..69ec35e6d 100644 --- a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs +++ b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs @@ -4,18 +4,24 @@ using Elsa.Http.Bookmarks; using Elsa.Http.Middleware; using Elsa.Http.Options; using Elsa.Workflows; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Models; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.State; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using NSubstitute; namespace Elsa.Http.UnitTests.Middleware; public class HttpWorkflowsMiddlewareTests { - private static readonly MethodInfo ExecuteWithinTimeoutAsyncMethod = typeof(HttpWorkflowsMiddleware).GetMethod("ExecuteWithinTimeoutAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly MethodInfo HandleWorkflowFaultAsyncMethod = GetRequiredPrivateMethod("HandleWorkflowFaultAsync"); + private static readonly MethodInfo ExecuteWithinTimeoutAsyncMethod = GetRequiredPrivateMethod("ExecuteWithinTimeoutAsync"); private const string CurrentTenantId = "tenant-a"; private const string OtherTenantId = "tenant-b"; private const string BookmarkHash = "http-endpoint:/colliding:get"; @@ -56,6 +62,73 @@ public class HttpWorkflowsMiddlewareTests Assert.False(filter.TenantAgnostic); } + [Fact] + public async Task HandleWorkflowFaultAsync_UsesReloadedWorkflowState_WhenAvailable() + { + var workflowState = CreateFaultedWorkflowState("workflow-1"); + var reloadedWorkflowState = CreateFaultedWorkflowState(workflowState.Id); + var workflowInstanceManager = Substitute.For(); + var httpEndpointFaultHandler = Substitute.For(); + var serviceProvider = new ServiceCollection() + .AddSingleton(workflowInstanceManager) + .AddSingleton(httpEndpointFaultHandler) + .BuildServiceProvider(); + var httpContext = new DefaultHttpContext(); + var workflowInstance = new WorkflowInstance + { + Id = workflowState.Id, + DefinitionId = workflowState.DefinitionId, + DefinitionVersionId = workflowState.DefinitionVersionId, + WorkflowState = reloadedWorkflowState + }; + + workflowInstanceManager.FindByIdAsync(workflowState.Id, Arg.Any()).Returns(Task.FromResult(workflowInstance)); + + var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None); + + Assert.True(handled); + await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is(context => ReferenceEquals(context.WorkflowState, reloadedWorkflowState))); + } + + [Fact] + public async Task HandleWorkflowFaultAsync_FallsBackToExecutionResultState_WhenReloadReturnsNull() + { + var workflowState = CreateFaultedWorkflowState("workflow-2"); + var workflowInstanceManager = Substitute.For(); + var httpEndpointFaultHandler = Substitute.For(); + var serviceProvider = new ServiceCollection() + .AddSingleton(workflowInstanceManager) + .AddSingleton(httpEndpointFaultHandler) + .BuildServiceProvider(); + var httpContext = new DefaultHttpContext(); + + workflowInstanceManager.FindByIdAsync(workflowState.Id, Arg.Any()).Returns(Task.FromResult(null)); + + var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None); + + Assert.True(handled); + await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is(context => ReferenceEquals(context.WorkflowState, workflowState))); + } + + private async Task HandleWorkflowFaultAsync(IServiceProvider serviceProvider, HttpContext httpContext, RunWorkflowResult workflowExecutionResult, CancellationToken cancellationToken) + { + var task = (Task)HandleWorkflowFaultAsyncMethod.Invoke(_middleware, [serviceProvider, httpContext, workflowExecutionResult, cancellationToken])!; + return await task; + } + + private static RunWorkflowResult CreateRunWorkflowResult(WorkflowState workflowState) => new(default!, workflowState, default!, null, Journal.Empty); + + private static WorkflowState CreateFaultedWorkflowState(string id) => new() + { + Id = id, + DefinitionId = "definition", + DefinitionVersionId = "definition-version", + Incidents = new List + { + new("activity", "activity-node", "TestActivity", "Boom", null, DateTimeOffset.UtcNow) + } + }; + [Fact] public async Task ExecuteWithinTimeoutAsync_RestoresRequestAbortedAfterSuccess() { @@ -113,6 +186,9 @@ public class HttpWorkflowsMiddlewareTests return await task; } + private static MethodInfo GetRequiredPrivateMethod(string name) => + typeof(HttpWorkflowsMiddleware).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new MissingMethodException(typeof(HttpWorkflowsMiddleware).FullName, name); + private static IEnumerable CreateCollidingHttpEndpointBookmarks() { yield return CreateBookmark("current-tenant-bookmark", CurrentTenantId); From 55284aa1c1aac0fb3e87a76b1a68664241e65881 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 19 Jun 2026 23:33:22 +0200 Subject: [PATCH 11/33] Add stable application instance name configuration Forward-port the opt-in stable application instance name support from PR #7734 so clustered deployments can reuse per-instance transport entities across restarts while preserving random names by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Elsa.sln | 15 ++ .../Features/ClusteringFeature.cs | 14 +- .../Options/ApplicationInstanceOptions.cs | 45 ++++ ...nfiguredApplicationInstanceNameProvider.cs | 99 ++++++++ .../ShellFeatures/ClusteringFeature.cs | 15 +- .../Elsa.Hosting.Management.UnitTests.csproj | 13 + ...redApplicationInstanceNameProviderTests.cs | 229 ++++++++++++++++++ 7 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs create mode 100644 src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs create mode 100644 test/unit/Elsa.Hosting.Management.UnitTests/Elsa.Hosting.Management.UnitTests.csproj create mode 100644 test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs diff --git a/Elsa.sln b/Elsa.sln index dcb35ca98..4c0db89a7 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -411,6 +411,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Exte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Runtime", "src\modules\Elsa.Persistence.VNext.Runtime\Elsa.Persistence.VNext.Runtime.csproj", "{6E3B6948-B16D-480C-879E-B805F732649F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Hosting.Management.UnitTests", "test\unit\Elsa.Hosting.Management.UnitTests\Elsa.Hosting.Management.UnitTests.csproj", "{39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1823,6 +1825,18 @@ Global {6E3B6948-B16D-480C-879E-B805F732649F}.Release|x64.Build.0 = Release|Any CPU {6E3B6948-B16D-480C-879E-B805F732649F}.Release|x86.ActiveCfg = Release|Any CPU {6E3B6948-B16D-480C-879E-B805F732649F}.Release|x86.Build.0 = Release|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Debug|x64.Build.0 = Debug|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Debug|x86.Build.0 = Debug|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|Any CPU.Build.0 = Release|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x64.ActiveCfg = Release|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x64.Build.0 = Release|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x86.ActiveCfg = Release|Any CPU + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1978,6 +1992,7 @@ Global {D0F9978C-9F75-48C2-88F6-5B7813D5AC74} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {E1607923-038B-41D6-9D23-F540FC9E6CCE} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {6E3B6948-B16D-480C-879E-B805F732649F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6} = {18453B51-25EB-4317-A4B3-B10518252E92} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.Hosting.Management/Features/ClusteringFeature.cs b/src/modules/Elsa.Hosting.Management/Features/ClusteringFeature.cs index 4a38e9eed..ad900c3ff 100644 --- a/src/modules/Elsa.Hosting.Management/Features/ClusteringFeature.cs +++ b/src/modules/Elsa.Hosting.Management/Features/ClusteringFeature.cs @@ -21,9 +21,14 @@ public class ClusteringFeature : FeatureBase /// /// A factory that instantiates an . /// + /// + /// Defaults to , which honors + /// for a stable instance name and falls back to a random + /// name when none is configured (preserving the previous default behaviour). + /// public Func InstanceNameProvider { get; set; } = sp => { - return ActivatorUtilities.CreateInstance(sp); + return ActivatorUtilities.CreateInstance(sp); }; /// @@ -31,6 +36,12 @@ public class ClusteringFeature : FeatureBase /// public Action HeartbeatOptions { get; set; } = _ => { }; + /// + /// Configures how the application instance name is determined. Set a stable name (for example from + /// the pod name) to avoid accumulating orphaned per-instance transport entities across restarts. + /// + public Action ApplicationInstanceOptions { get; set; } = _ => { }; + /// public override void ConfigureHostedServices() { @@ -42,6 +53,7 @@ public class ClusteringFeature : FeatureBase public override void Apply() { Services.Configure(HeartbeatOptions) + .Configure(ApplicationInstanceOptions) .AddSingleton(InstanceNameProvider) .AddSingleton(); } diff --git a/src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs b/src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs new file mode 100644 index 000000000..338c8d0bf --- /dev/null +++ b/src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs @@ -0,0 +1,45 @@ +namespace Elsa.Hosting.Management.Options; + +/// +/// Options that control how the name of the current application instance is determined. +/// +/// +/// The instance name is used to name per-instance transport entities, such as the Azure Service Bus +/// change-token subscription and queue ({instanceName}-elsa-trigger-change-token-signal). +/// By default a random name is generated for every process start, which means a new entity is created +/// on every restart. Under transports with a per-topic entity limit (for example Azure Service Bus, +/// which caps a topic at 2,000 subscriptions), these orphaned entities can accumulate across restarts +/// until the limit is reached and new instances can no longer start. Providing a stable name +/// that is reused across restarts of the same logical instance keeps the number of entities bounded. +/// +/// A stable name must be both stable across restarts of the same instance and unique across instances +/// that run at the same time. In Kubernetes a StatefulSet provides exactly this (each pod keeps its +/// ordinal hostname across restarts); for a Deployment the pod name can be projected via the Downward +/// API (for example metadata.name). +/// +public class ApplicationInstanceOptions +{ + /// + /// An explicit, stable, unique-per-instance name. When set, this value is used directly and takes + /// precedence over . + /// + /// + /// Keep this value short enough for downstream transport entity names. For Azure Service Bus, the + /// change-token subscription name must fit in 50 characters, leaving 17 characters for this prefix. + /// Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a + /// letter or number. + /// + public string? InstanceName { get; set; } + + /// + /// The name of an environment variable to read the instance name from when + /// is not set. For example, set this to HOSTNAME to use the Kubernetes pod name (stable across + /// restarts when running as a StatefulSet). When or empty, no environment + /// variable is read and a random name is generated instead. + /// + /// + /// The environment variable name is trimmed before lookup. The value it contains follows the same + /// transport entity-name length constraints as . + /// + public string? InstanceNameEnvironmentVariable { get; set; } +} diff --git a/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs b/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs new file mode 100644 index 000000000..dd44c96d5 --- /dev/null +++ b/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs @@ -0,0 +1,99 @@ +using Elsa.Hosting.Management.Contracts; +using Elsa.Hosting.Management.Options; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Elsa.Hosting.Management.Services; + +/// +/// Resolves the application instance name from , allowing a +/// stable name to be configured so that per-instance transport entities are reused across restarts +/// instead of accumulating. Falls back to a random name when no stable name is configured, which +/// preserves the previous default behaviour. +/// +/// +/// Resolution order: +/// +/// when set. +/// The environment variable named by when configured and non-empty. +/// A randomly generated name (legacy behaviour). +/// +/// +public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNameProvider +{ + private const int AzureServiceBusSubscriptionNameMaxLength = 50; + private const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-trigger-change-token-signal"; + private static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerChangeTokenSignalEndpointNameSuffix.Length; + + private readonly string _instanceName; + + /// + /// Initializes a new instance of the class. + /// + public ConfiguredApplicationInstanceNameProvider( + IOptions options, + RandomIntIdentityGenerator randomIdentityGenerator, + ILogger logger) + { + var value = options.Value; + + if (!string.IsNullOrWhiteSpace(value.InstanceName)) + { + _instanceName = ValidateConfiguredInstanceName(value.InstanceName, $"{nameof(ApplicationInstanceOptions)}.{nameof(ApplicationInstanceOptions.InstanceName)}"); + return; + } + + if (!string.IsNullOrWhiteSpace(value.InstanceNameEnvironmentVariable)) + { + var environmentVariable = value.InstanceNameEnvironmentVariable.Trim(); + var fromEnvironment = Environment.GetEnvironmentVariable(environmentVariable); + + if (!string.IsNullOrWhiteSpace(fromEnvironment)) + { + _instanceName = ValidateConfiguredInstanceName(fromEnvironment, $"environment variable '{environmentVariable}'"); + return; + } + + logger.LogWarning( + "The configured instance-name environment variable '{EnvironmentVariable}' is not set or empty. Falling back to a random instance name. " + + "A random name causes per-instance transport entities (such as the Azure Service Bus change-token subscription) to be recreated on every restart, " + + "which can accumulate until the transport's per-topic limit is reached.", + environmentVariable); + } + + _instanceName = randomIdentityGenerator.GenerateId(); + } + + /// + public string GetName() => _instanceName; + + private static string ValidateConfiguredInstanceName(string value, string source) + { + var instanceName = value.Trim(); + + if (instanceName.Length <= ConfiguredInstanceNameMaxLength) + { + if (IsValidConfiguredInstanceName(instanceName)) + return instanceName; + + throw new InvalidOperationException( + $"The configured application instance name from {source} contains invalid characters. " + + "Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a letter or number."); + } + + throw new InvalidOperationException( + $"The configured application instance name from {source} is {instanceName.Length} characters long, but it must be {ConfiguredInstanceNameMaxLength} characters or fewer. " + + $"The value is used to create per-instance transport entities such as '{instanceName}{TriggerChangeTokenSignalEndpointNameSuffix}', which must fit within Azure Service Bus's {AzureServiceBusSubscriptionNameMaxLength}-character subscription name limit. " + + "Configure a shorter stable name that is still unique for each concurrently running instance."); + } + + private static bool IsValidConfiguredInstanceName(string instanceName) + { + return IsAsciiLetterOrDigit(instanceName[0]) + && IsAsciiLetterOrDigit(instanceName[^1]) + && instanceName.All(c => IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_'); + } + + private static bool IsAsciiLetterOrDigit(char value) => + value is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9'; +} diff --git a/src/modules/Elsa.Hosting.Management/ShellFeatures/ClusteringFeature.cs b/src/modules/Elsa.Hosting.Management/ShellFeatures/ClusteringFeature.cs index fc6eae7f4..b15c93c93 100644 --- a/src/modules/Elsa.Hosting.Management/ShellFeatures/ClusteringFeature.cs +++ b/src/modules/Elsa.Hosting.Management/ShellFeatures/ClusteringFeature.cs @@ -20,9 +20,14 @@ public class ClusteringFeature : IShellFeature /// /// A factory that instantiates an . /// + /// + /// Defaults to , which honors + /// for a stable instance name and falls back to a random + /// name when none is configured (preserving the previous default behaviour). + /// public Func InstanceNameProvider { get; set; } = sp => { - return ActivatorUtilities.CreateInstance(sp); + return ActivatorUtilities.CreateInstance(sp); }; /// @@ -30,13 +35,19 @@ public class ClusteringFeature : IShellFeature /// public Action HeartbeatOptions { get; set; } = _ => { }; + /// + /// Configures how the application instance name is determined. Set a stable name (for example from + /// the pod name) to avoid accumulating orphaned per-instance transport entities across restarts. + /// + public Action ApplicationInstanceOptions { get; set; } = _ => { }; + public void ConfigureServices(IServiceCollection services) { services.Configure(HeartbeatOptions) + .Configure(ApplicationInstanceOptions) .AddSingleton(InstanceNameProvider) .AddSingleton() .AddHostedService() .AddHostedService(); } } - diff --git a/test/unit/Elsa.Hosting.Management.UnitTests/Elsa.Hosting.Management.UnitTests.csproj b/test/unit/Elsa.Hosting.Management.UnitTests/Elsa.Hosting.Management.UnitTests.csproj new file mode 100644 index 000000000..46fc08991 --- /dev/null +++ b/test/unit/Elsa.Hosting.Management.UnitTests/Elsa.Hosting.Management.UnitTests.csproj @@ -0,0 +1,13 @@ + + + + [Elsa.Hosting.Management]* + 0 + + + + + + + + diff --git a/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs b/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs new file mode 100644 index 000000000..4446df2fe --- /dev/null +++ b/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs @@ -0,0 +1,229 @@ +using Elsa.Hosting.Management.Contracts; +using Elsa.Hosting.Management.Options; +using Elsa.Hosting.Management.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using ShellClusteringFeature = Elsa.Hosting.Management.ShellFeatures.ClusteringFeature; + +namespace Elsa.Hosting.Management.UnitTests.Services; + +public class ConfiguredApplicationInstanceNameProviderTests +{ + private const int AzureServiceBusSubscriptionNameMaxLength = 50; + private const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-trigger-change-token-signal"; + private static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerChangeTokenSignalEndpointNameSuffix.Length; + + [Fact] + public void ExplicitInstanceName_IsUsedDirectly() + { + var provider = CreateProvider(new() + { + InstanceName = "pod-0", + InstanceNameEnvironmentVariable = "ELSA_TEST_INSTANCE_NAME" + }); + + Assert.Equal("pod-0", provider.GetName()); + } + + [Fact] + public void ExplicitInstanceName_IsTrimmed() + { + var provider = CreateProvider(new() + { + InstanceName = " pod-0 " + }); + + Assert.Equal("pod-0", provider.GetName()); + } + + [Fact] + public void ExplicitInstanceName_TakesPrecedenceOverEnvironmentVariable() + { + var variable = NewVariableName(); + Environment.SetEnvironmentVariable(variable, "from-env"); + + try + { + var provider = CreateProvider(new() + { + InstanceName = "explicit", + InstanceNameEnvironmentVariable = variable + }); + + Assert.Equal("explicit", provider.GetName()); + } + finally + { + Environment.SetEnvironmentVariable(variable, null); + } + } + + [Fact] + public void EnvironmentVariable_IsUsedWhenInstanceNameNotSet() + { + var variable = NewVariableName(); + Environment.SetEnvironmentVariable(variable, "pod-7"); + + try + { + var provider = CreateProvider(new() + { + InstanceNameEnvironmentVariable = variable + }); + + Assert.Equal("pod-7", provider.GetName()); + } + finally + { + Environment.SetEnvironmentVariable(variable, null); + } + } + + [Fact] + public void EnvironmentVariableName_IsTrimmed() + { + var variable = NewVariableName(); + Environment.SetEnvironmentVariable(variable, "pod-7"); + + try + { + var provider = CreateProvider(new() + { + InstanceNameEnvironmentVariable = $" {variable} " + }); + + Assert.Equal("pod-7", provider.GetName()); + } + finally + { + Environment.SetEnvironmentVariable(variable, null); + } + } + + [Fact] + public void EnvironmentVariable_ValueIsTrimmed() + { + var variable = NewVariableName(); + Environment.SetEnvironmentVariable(variable, " pod-7 "); + + try + { + var provider = CreateProvider(new() + { + InstanceNameEnvironmentVariable = variable + }); + + Assert.Equal("pod-7", provider.GetName()); + } + finally + { + Environment.SetEnvironmentVariable(variable, null); + } + } + + [Fact] + public void NoConfiguration_FallsBackToRandomName() + { + var name1 = CreateProvider(new()).GetName(); + var name2 = CreateProvider(new()).GetName(); + + Assert.False(string.IsNullOrWhiteSpace(name1)); + Assert.False(string.IsNullOrWhiteSpace(name2)); + Assert.NotEqual(name1, name2); + } + + [Fact] + public void EnvironmentVariableConfiguredButEmpty_FallsBackToRandomName() + { + var variable = NewVariableName(); + Environment.SetEnvironmentVariable(variable, null); + + var name1 = CreateProvider(new() { InstanceNameEnvironmentVariable = variable }).GetName(); + var name2 = CreateProvider(new() { InstanceNameEnvironmentVariable = variable }).GetName(); + + Assert.False(string.IsNullOrWhiteSpace(name1)); + Assert.NotEqual(name1, name2); + } + + [Fact] + public void ExplicitInstanceName_AtMaximumLength_IsAccepted() + { + var instanceName = new string('a', ConfiguredInstanceNameMaxLength); + + var provider = CreateProvider(new() { InstanceName = instanceName }); + + Assert.Equal(instanceName, provider.GetName()); + } + + [Fact] + public void ExplicitInstanceName_TooLong_Throws() + { + var instanceName = new string('a', ConfiguredInstanceNameMaxLength + 1); + + var exception = Assert.Throws(() => CreateProvider(new() { InstanceName = instanceName })); + + Assert.Contains($"{ConfiguredInstanceNameMaxLength} characters or fewer", exception.Message); + Assert.Contains("Azure Service Bus", exception.Message); + } + + [Theory] + [InlineData("pod 0")] + [InlineData("pöd-0")] + [InlineData("-pod-0")] + [InlineData("pod-0-")] + public void ExplicitInstanceName_InvalidCharacters_Throws(string instanceName) + { + var exception = Assert.Throws(() => CreateProvider(new() { InstanceName = instanceName })); + + Assert.Contains("contains invalid characters", exception.Message); + } + + [Fact] + public void EnvironmentVariableValue_TooLong_Throws() + { + var variable = NewVariableName(); + Environment.SetEnvironmentVariable(variable, new string('a', ConfiguredInstanceNameMaxLength + 1)); + + try + { + var exception = Assert.Throws(() => CreateProvider(new() { InstanceNameEnvironmentVariable = variable })); + + Assert.Contains(variable, exception.Message); + Assert.Contains($"{ConfiguredInstanceNameMaxLength} characters or fewer", exception.Message); + } + finally + { + Environment.SetEnvironmentVariable(variable, null); + } + } + + [Fact] + public void ShellClusteringFeature_UsesConfiguredInstanceNameProvider() + { + var services = new ServiceCollection(); + var feature = new ShellClusteringFeature + { + ApplicationInstanceOptions = options => options.InstanceName = "pod-0" + }; + + services.AddLogging(); + feature.ConfigureServices(services); + + using var serviceProvider = services.BuildServiceProvider(); + + var provider = serviceProvider.GetRequiredService(); + + Assert.IsType(provider); + Assert.Equal("pod-0", provider.GetName()); + } + + private static ConfiguredApplicationInstanceNameProvider CreateProvider(ApplicationInstanceOptions options) + { + return new ConfiguredApplicationInstanceNameProvider( + Microsoft.Extensions.Options.Options.Create(options), + new RandomIntIdentityGenerator(), + NullLogger.Instance); + } + + private static string NewVariableName() => "ELSA_TEST_INSTANCE_" + Guid.NewGuid().ToString("N"); +} From b08b10132c89363c196aa518e269ad097de79f25 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 20 Jun 2026 00:13:33 +0200 Subject: [PATCH 12/33] address greptile review feedback (greploop iteration 1) - Make validation constants internal and expose via InternalsVisibleTo so tests reference single source of truth - Add empty-string guard to IsValidConfiguredInstanceName to prevent IndexOutOfRangeException - Add ClusteringFeature_UsesConfiguredInstanceNameProvider test for Features.ClusteringFeature to match existing ShellFeatures coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Elsa.Hosting.Management/AssemblyInfo.cs | 3 ++ ...nfiguredApplicationInstanceNameProvider.cs | 9 ++++-- ...redApplicationInstanceNameProviderTests.cs | 32 +++++++++++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/modules/Elsa.Hosting.Management/AssemblyInfo.cs diff --git a/src/modules/Elsa.Hosting.Management/AssemblyInfo.cs b/src/modules/Elsa.Hosting.Management/AssemblyInfo.cs new file mode 100644 index 000000000..487b06135 --- /dev/null +++ b/src/modules/Elsa.Hosting.Management/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Elsa.Hosting.Management.UnitTests")] diff --git a/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs b/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs index dd44c96d5..2fb7ef941 100644 --- a/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs +++ b/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs @@ -21,9 +21,9 @@ namespace Elsa.Hosting.Management.Services; /// public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNameProvider { - private const int AzureServiceBusSubscriptionNameMaxLength = 50; - private const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-trigger-change-token-signal"; - private static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerChangeTokenSignalEndpointNameSuffix.Length; + internal const int AzureServiceBusSubscriptionNameMaxLength = 50; + internal const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-trigger-change-token-signal"; + internal static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerChangeTokenSignalEndpointNameSuffix.Length; private readonly string _instanceName; @@ -89,6 +89,9 @@ public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNam private static bool IsValidConfiguredInstanceName(string instanceName) { + if (instanceName.Length == 0) + return false; + return IsAsciiLetterOrDigit(instanceName[0]) && IsAsciiLetterOrDigit(instanceName[^1]) && instanceName.All(c => IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_'); diff --git a/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs b/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs index 4446df2fe..901496b8e 100644 --- a/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs +++ b/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs @@ -1,17 +1,20 @@ +using Elsa.Features.Services; using Elsa.Hosting.Management.Contracts; using Elsa.Hosting.Management.Options; using Elsa.Hosting.Management.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using ClusteringFeature = Elsa.Hosting.Management.Features.ClusteringFeature; using ShellClusteringFeature = Elsa.Hosting.Management.ShellFeatures.ClusteringFeature; namespace Elsa.Hosting.Management.UnitTests.Services; public class ConfiguredApplicationInstanceNameProviderTests { - private const int AzureServiceBusSubscriptionNameMaxLength = 50; - private const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-trigger-change-token-signal"; - private static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerChangeTokenSignalEndpointNameSuffix.Length; + private static int AzureServiceBusSubscriptionNameMaxLength => ConfiguredApplicationInstanceNameProvider.AzureServiceBusSubscriptionNameMaxLength; + private static string TriggerChangeTokenSignalEndpointNameSuffix => ConfiguredApplicationInstanceNameProvider.TriggerChangeTokenSignalEndpointNameSuffix; + private static int ConfiguredInstanceNameMaxLength => ConfiguredApplicationInstanceNameProvider.ConfiguredInstanceNameMaxLength; [Fact] public void ExplicitInstanceName_IsUsedDirectly() @@ -217,6 +220,29 @@ public class ConfiguredApplicationInstanceNameProviderTests Assert.Equal("pod-0", provider.GetName()); } + [Fact] + public void ClusteringFeature_UsesConfiguredInstanceNameProvider() + { + var services = new ServiceCollection(); + var module = Substitute.For(); + module.Services.Returns(services); + + var feature = new ClusteringFeature(module) + { + ApplicationInstanceOptions = options => options.InstanceName = "pod-0" + }; + + services.AddLogging(); + feature.Apply(); + + using var serviceProvider = services.BuildServiceProvider(); + + var provider = serviceProvider.GetRequiredService(); + + Assert.IsType(provider); + Assert.Equal("pod-0", provider.GetName()); + } + private static ConfiguredApplicationInstanceNameProvider CreateProvider(ApplicationInstanceOptions options) { return new ConfiguredApplicationInstanceNameProvider( From 29d43dadbb840dc2b25b06b98acd2710c9e49c2a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 20 Jun 2026 00:58:40 +0200 Subject: [PATCH 13/33] Update CShells and Elsa.PackageManifest.Generator to latest versions --- Directory.Packages.props | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2539e692f..a01eb1908 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -106,13 +106,13 @@ - - - - - - - + + + + + + + From 571c1c7b2cb3f2fa418f230341592243ad2020ef Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 20 Jun 2026 12:50:02 +0200 Subject: [PATCH 14/33] Shorten configured application instance names Configured stable application instance names that exceed the Azure Service Bus transport entity limit are now shortened deterministically instead of failing startup. The same configured value resolves to the same shortened name across restarts, preserving stable per-instance transport identity while supporting normal Kubernetes pod names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Options/ApplicationInstanceOptions.cs | 10 ++--- ...nfiguredApplicationInstanceNameProvider.cs | 44 +++++++++++++------ ...redApplicationInstanceNameProviderTests.cs | 26 ++++++----- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs b/src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs index 338c8d0bf..fb54c3e81 100644 --- a/src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs +++ b/src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs @@ -5,7 +5,7 @@ namespace Elsa.Hosting.Management.Options; /// /// /// The instance name is used to name per-instance transport entities, such as the Azure Service Bus -/// change-token subscription and queue ({instanceName}-elsa-trigger-change-token-signal). +/// change-token subscription and queue ({instanceName}-elsa-tct). /// By default a random name is generated for every process start, which means a new entity is created /// on every restart. Under transports with a per-topic entity limit (for example Azure Service Bus, /// which caps a topic at 2,000 subscriptions), these orphaned entities can accumulate across restarts @@ -24,10 +24,10 @@ public class ApplicationInstanceOptions /// precedence over . /// /// - /// Keep this value short enough for downstream transport entity names. For Azure Service Bus, the - /// change-token subscription name must fit in 50 characters, leaving 17 characters for this prefix. /// Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a - /// letter or number. + /// letter or number. Values that are too long for downstream transport entity names are shortened + /// deterministically so the same configured value resolves to the same application instance name + /// across restarts. /// public string? InstanceName { get; set; } @@ -39,7 +39,7 @@ public class ApplicationInstanceOptions /// /// /// The environment variable name is trimmed before lookup. The value it contains follows the same - /// transport entity-name length constraints as . + /// character rules and deterministic shortening behavior as . /// public string? InstanceNameEnvironmentVariable { get; set; } } diff --git a/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs b/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs index 2fb7ef941..bd263f88a 100644 --- a/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs +++ b/src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs @@ -2,6 +2,7 @@ using Elsa.Hosting.Management.Contracts; using Elsa.Hosting.Management.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System.Security.Cryptography; namespace Elsa.Hosting.Management.Services; @@ -22,7 +23,8 @@ namespace Elsa.Hosting.Management.Services; public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNameProvider { internal const int AzureServiceBusSubscriptionNameMaxLength = 50; - internal const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-trigger-change-token-signal"; + internal const string TriggerChangeTokenSignalEndpointNameSuffix = "-elsa-tct"; + private const int ShortenedNameHashLength = 16; internal static readonly int ConfiguredInstanceNameMaxLength = AzureServiceBusSubscriptionNameMaxLength - TriggerChangeTokenSignalEndpointNameSuffix.Length; private readonly string _instanceName; @@ -39,7 +41,7 @@ public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNam if (!string.IsNullOrWhiteSpace(value.InstanceName)) { - _instanceName = ValidateConfiguredInstanceName(value.InstanceName, $"{nameof(ApplicationInstanceOptions)}.{nameof(ApplicationInstanceOptions.InstanceName)}"); + _instanceName = ResolveConfiguredInstanceName(value.InstanceName, $"{nameof(ApplicationInstanceOptions)}.{nameof(ApplicationInstanceOptions.InstanceName)}", logger); return; } @@ -50,7 +52,7 @@ public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNam if (!string.IsNullOrWhiteSpace(fromEnvironment)) { - _instanceName = ValidateConfiguredInstanceName(fromEnvironment, $"environment variable '{environmentVariable}'"); + _instanceName = ResolveConfiguredInstanceName(fromEnvironment, $"environment variable '{environmentVariable}'", logger); return; } @@ -67,24 +69,29 @@ public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNam /// public string GetName() => _instanceName; - private static string ValidateConfiguredInstanceName(string value, string source) + private static string ResolveConfiguredInstanceName(string value, string source, ILogger logger) { var instanceName = value.Trim(); - if (instanceName.Length <= ConfiguredInstanceNameMaxLength) - { - if (IsValidConfiguredInstanceName(instanceName)) - return instanceName; - + if (!IsValidConfiguredInstanceName(instanceName)) throw new InvalidOperationException( $"The configured application instance name from {source} contains invalid characters. " + "Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a letter or number."); - } - throw new InvalidOperationException( - $"The configured application instance name from {source} is {instanceName.Length} characters long, but it must be {ConfiguredInstanceNameMaxLength} characters or fewer. " + - $"The value is used to create per-instance transport entities such as '{instanceName}{TriggerChangeTokenSignalEndpointNameSuffix}', which must fit within Azure Service Bus's {AzureServiceBusSubscriptionNameMaxLength}-character subscription name limit. " + - "Configure a shorter stable name that is still unique for each concurrently running instance."); + if (instanceName.Length <= ConfiguredInstanceNameMaxLength) + return instanceName; + + var shortenedName = ShortenConfiguredInstanceName(instanceName); + + logger.LogWarning( + "The configured application instance name from {Source} is {Length} characters long, exceeding the {MaxLength}-character limit required for Azure Service Bus transport entities. " + + "Using deterministic shortened instance name '{ShortenedName}' instead.", + source, + instanceName.Length, + ConfiguredInstanceNameMaxLength, + shortenedName); + + return shortenedName; } private static bool IsValidConfiguredInstanceName(string instanceName) @@ -99,4 +106,13 @@ public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNam private static bool IsAsciiLetterOrDigit(char value) => value is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9'; + + private static string ShortenConfiguredInstanceName(string instanceName) + { + var hash = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(instanceName))).ToLowerInvariant()[..ShortenedNameHashLength]; + var prefixLength = ConfiguredInstanceNameMaxLength - hash.Length - 1; + var prefix = instanceName[..prefixLength]; + + return $"{prefix}-{hash}"; + } } diff --git a/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs b/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs index 901496b8e..9d85f8df0 100644 --- a/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs +++ b/test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs @@ -159,14 +159,17 @@ public class ConfiguredApplicationInstanceNameProviderTests } [Fact] - public void ExplicitInstanceName_TooLong_Throws() + public void ExplicitInstanceName_TooLong_IsShortenedDeterministically() { - var instanceName = new string('a', ConfiguredInstanceNameMaxLength + 1); + var instanceName = "nexxbiz-executor-api-v3-1-extra-long-replica-0001"; - var exception = Assert.Throws(() => CreateProvider(new() { InstanceName = instanceName })); + var name1 = CreateProvider(new() { InstanceName = instanceName }).GetName(); + var name2 = CreateProvider(new() { InstanceName = instanceName }).GetName(); - Assert.Contains($"{ConfiguredInstanceNameMaxLength} characters or fewer", exception.Message); - Assert.Contains("Azure Service Bus", exception.Message); + Assert.Equal(name1, name2); + Assert.True(name1.Length <= ConfiguredInstanceNameMaxLength); + Assert.StartsWith(instanceName[..8], name1); + Assert.NotEqual(instanceName, name1); } [Theory] @@ -182,17 +185,20 @@ public class ConfiguredApplicationInstanceNameProviderTests } [Fact] - public void EnvironmentVariableValue_TooLong_Throws() + public void EnvironmentVariableValue_TooLong_IsShortenedDeterministically() { var variable = NewVariableName(); - Environment.SetEnvironmentVariable(variable, new string('a', ConfiguredInstanceNameMaxLength + 1)); + var instanceName = "nexxbiz-executor-api-v3-1-extra-long-replica-0001"; + Environment.SetEnvironmentVariable(variable, instanceName); try { - var exception = Assert.Throws(() => CreateProvider(new() { InstanceNameEnvironmentVariable = variable })); + var name1 = CreateProvider(new() { InstanceNameEnvironmentVariable = variable }).GetName(); + var name2 = CreateProvider(new() { InstanceNameEnvironmentVariable = variable }).GetName(); - Assert.Contains(variable, exception.Message); - Assert.Contains($"{ConfiguredInstanceNameMaxLength} characters or fewer", exception.Message); + Assert.Equal(name1, name2); + Assert.True(name1.Length <= ConfiguredInstanceNameMaxLength); + Assert.NotEqual(instanceName, name1); } finally { From 5cc2a7c75a399ed774eb59b3d236ede8a1df25f5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 21 Jun 2026 23:18:28 +0200 Subject: [PATCH 15/33] Fix publish event payload assertion casing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Primitives/Event/PublishEventTests.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs index b1390b510..18782b151 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs @@ -74,10 +74,25 @@ public class PublishEventTests : AppComponentTest // Verify the payload structure and content using var payloadDocument = JsonDocument.Parse(JsonSerializer.Serialize(receivedPayload)); - Assert.True(payloadDocument.RootElement.TryGetProperty("Status", out var status), "Received payload should contain a Status property"); + Assert.True(TryGetProperty(payloadDocument.RootElement, "Status", out var status), "Received payload should contain a Status property"); Assert.Equal("Shipped", status.GetString()); } + private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement value) + { + foreach (var property in element.EnumerateObject()) + { + if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + value = property.Value; + return true; + } + } + + value = default; + return false; + } + private async Task GetSingleWorkflowInstanceAsync(string definitionId, string correlationId, int timeoutMs = 5000) { var tcs = new TaskCompletionSource(); From b05eb3096b0bf616c241f5a7d78d5cb97e7b7087 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 21 Jun 2026 23:18:33 +0200 Subject: [PATCH 16/33] Fix flaky publish event payload test Make the PublishEvent payload assertion case-insensitive so it tolerates JsonPayloadSerializer camelCase output after a JSON round trip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Primitives/Event/PublishEventTests.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs index b1390b510..db0e44fe6 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs @@ -74,10 +74,25 @@ public class PublishEventTests : AppComponentTest // Verify the payload structure and content using var payloadDocument = JsonDocument.Parse(JsonSerializer.Serialize(receivedPayload)); - Assert.True(payloadDocument.RootElement.TryGetProperty("Status", out var status), "Received payload should contain a Status property"); + Assert.True(TryGetProperty(payloadDocument.RootElement, "Status", out var status), "Received payload should contain a Status property"); Assert.Equal("Shipped", status.GetString()); } + private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement property) + { + foreach (var candidate in element.EnumerateObject()) + { + if (candidate.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + { + property = candidate.Value; + return true; + } + } + + property = default; + return false; + } + private async Task GetSingleWorkflowInstanceAsync(string definitionId, string correlationId, int timeoutMs = 5000) { var tcs = new TaskCompletionSource(); From 2a5b716ba041fd6cd790f9cb3b7ace1bcd1e1bb3 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 21 Jun 2026 23:56:47 +0200 Subject: [PATCH 17/33] Allow NuGet.org for CShells packages Map CShells package IDs to NuGet.org as well as the CShells Feedz source so solution restore can resolve published CShells packages when the Feedz source has no matching package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- NuGet.Config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NuGet.Config b/NuGet.Config index 6a0629d8a..67a9c924d 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -11,6 +11,8 @@ + + From 635c3ea42f67e89a01b33f4b54015e0102337931 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 22 Jun 2026 00:44:36 +0200 Subject: [PATCH 18/33] Address PR review feedback Use LINQ for the case-insensitive payload property lookup and keep CShells package source mapping deterministic by mapping CShells packages only to nuget.org. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- NuGet.Config | 4 ---- .../Primitives/Event/PublishEventTests.cs | 17 +++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/NuGet.Config b/NuGet.Config index 67a9c924d..36cf83e1b 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -14,10 +14,6 @@ - - - - diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs index db0e44fe6..da6ee232f 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Text.Json; using Elsa.Common.Models; using Elsa.Testing.Shared; @@ -80,17 +81,17 @@ public class PublishEventTests : AppComponentTest private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement property) { - foreach (var candidate in element.EnumerateObject()) + var candidate = element.EnumerateObject() + .Where(candidate => candidate.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(); + if (candidate.Value.ValueKind == JsonValueKind.Undefined) { - if (candidate.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) - { - property = candidate.Value; - return true; - } + property = default; + return false; } - property = default; - return false; + property = candidate.Value; + return true; } private async Task GetSingleWorkflowInstanceAsync(string definitionId, string correlationId, int timeoutMs = 5000) From 66911bb77f6fd3d64682f5dfb5c1b37b2799834a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 23 Jun 2026 02:21:29 +0200 Subject: [PATCH 19/33] Address PublishEvent payload review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Primitives/Event/PublishEventTests.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs index da6ee232f..3db4cb5ba 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs @@ -81,17 +81,19 @@ public class PublishEventTests : AppComponentTest private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement property) { - var candidate = element.EnumerateObject() - .Where(candidate => candidate.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + var matchingProperty = element + .EnumerateObject() + .Where(x => string.Equals(x.Name, propertyName, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); - if (candidate.Value.ValueKind == JsonValueKind.Undefined) + + if (matchingProperty.Name != null) { - property = default; - return false; + property = matchingProperty.Value; + return true; } - property = candidate.Value; - return true; + property = default; + return false; } private async Task GetSingleWorkflowInstanceAsync(string definitionId, string correlationId, int timeoutMs = 5000) From d5378d6ef90277a1747bb1f37889f50bb9e0da63 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 23 Jun 2026 02:22:54 +0200 Subject: [PATCH 20/33] Address payload assertion review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Primitives/Event/PublishEventTests.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs index 18782b151..d4358cc91 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/Activities/Primitives/Event/PublishEventTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Text.Json; using Elsa.Common.Models; using Elsa.Testing.Shared; @@ -80,17 +81,13 @@ public class PublishEventTests : AppComponentTest private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement value) { - foreach (var property in element.EnumerateObject()) - { - if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } + value = element + .EnumerateObject() + .Where(property => string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + .Select(property => property.Value) + .FirstOrDefault(); - value = default; - return false; + return value.ValueKind != JsonValueKind.Undefined; } private async Task GetSingleWorkflowInstanceAsync(string definitionId, string correlationId, int timeoutMs = 5000) From 82e069c265c0ae483fdc0899b0d89b680238218b Mon Sep 17 00:00:00 2001 From: MohitGuptaC <155074110+MohitGuptaC@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:13:51 +0530 Subject: [PATCH 21/33] fix: correct Oracle identifier quoting and NVARCHAR2 cast in GenerateOracleUpsert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unquoted aliases in SELECT … FROM DUAL caused ORA-00904 because Oracle uppercases bare identifiers. All aliases, ON condition, UPDATE SET, and INSERT/VALUES column references are now double-quoted to match the case-sensitive names EF Core migrations produce. NVARCHAR2 columns additionally required an explicit CAST because ODP.NET cannot infer bind parameter types from a FROM DUAL subquery and defaults to VARCHAR2. CAST(:p AS NVARCHAR2(n)) with length extracted from the EF column type string resolves the datatype mismatch. Both fixes are required — neither alone produces working Oracle persistence. All other providers are unchanged. fixes Fixes #7755 --- .../Extensions/BulkUpsertExtensions.cs | 213 +++++++++--------- 1 file changed, 103 insertions(+), 110 deletions(-) diff --git a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs index fadf509a7..2749790bd 100644 --- a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs +++ b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs @@ -16,12 +16,6 @@ public static class BulkUpsertExtensions /// /// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector. /// - /// The type of the database context. - /// The type of the entity being upserted. - /// The database context where the bulk upsert operation will be executed. - /// The list of entities to be upserted. - /// An expression used to determine the key for upsert operations. - /// A token to observe while waiting for the operation to complete. public static async Task BulkUpsertAsync( this TDbContext dbContext, IList entities, @@ -36,13 +30,6 @@ public static class BulkUpsertExtensions /// /// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector and optional batch size. /// - /// The type of the database context. - /// The type of the entity being upserted. - /// The database context where the bulk upsert operation will be executed. - /// The list of entities to be upserted. - /// An expression used to determine the key for upsert operations. - /// The size of each batch for processing the upsert operation. Defaults to 50. - /// A token to observe while waiting for the operation to complete. /// Thrown if the database provider for the context is not supported. public static async Task BulkUpsertAsync( this TDbContext dbContext, @@ -56,30 +43,29 @@ public static class BulkUpsertExtensions if (entities.Count == 0) return; - // Identify the current provider (e.g., "Microsoft.EntityFrameworkCore.SqlServer") var providerName = dbContext.Database.ProviderName?.ToLowerInvariant() ?? string.Empty; - // Determine the method for generating SQL based on the provider Func, Expression>, (string, object[])> generateSql = providerName switch { var pn when pn.Contains("sqlserver") => GenerateSqlServerUpsert, - var pn when pn.Contains("sqlite") => GenerateSqliteUpsert, - var pn when pn.Contains("postgres") => GeneratePostgresUpsert, - var pn when pn.Contains("mysql") => GenerateMySqlUpsert, - var pn when pn.Contains("oracle") => GenerateOracleUpsert, + var pn when pn.Contains("sqlite") => GenerateSqliteUpsert, + var pn when pn.Contains("postgres") => GeneratePostgresUpsert, + var pn when pn.Contains("mysql") => GenerateMySqlUpsert, + var pn when pn.Contains("oracle") => GenerateOracleUpsert, _ => throw new NotSupportedException($"Provider '{providerName}' is not supported.") }; - // Loop through batched entities foreach (var batch in entities.Chunk(batchSize)) { - // Generate SQL and parameters var (sql, parameters) = generateSql(dbContext, batch, keySelector); - await dbContext.Database.ExecuteSqlRawAsync(sql, parameters, cancellationToken); } } + // ------------------------------------------------------------------------- + // SQL Server + // ------------------------------------------------------------------------- + private static (string, object[]) GenerateSqlServerUpsert( DbContext dbContext, IList entities, @@ -92,9 +78,7 @@ public static class BulkUpsertExtensions var props = entityType.GetProperties().ToList(); var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = $"[{keyProp.GetColumnName(storeObject)}]"; - var columnNames = props - .Select(p => $"[{p.GetColumnName(storeObject)}]") - .ToList(); + var columnNames = props.Select(p => $"[{p.GetColumnName(storeObject)}]").ToList(); var mergeSql = new StringBuilder(); mergeSql.AppendLine($"MERGE {tableName} AS Target"); @@ -111,27 +95,21 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; - - // If it's a shadow property, retrieve value via Entry(..).Property(..) var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); - var converter = property.GetTypeMapping().Converter; - if (converter != null) - value = converter.ConvertToProvider(value)!; + if (converter != null) value = converter.ConvertToProvider(value)!; - // Explicitly cast null values for varbinary columns if (property.GetColumnType().StartsWith("varbinary", StringComparison.OrdinalIgnoreCase) && value is null) - values.Add("CAST(NULL AS varbinary(max))"); // Explicitly cast null + values.Add("CAST(NULL AS varbinary(max))"); else values.Add(paramName); - + parameters.Add(value!); } - var line = $"({string.Join(", ", values)}){(i < entities.Count - 1 ? "," : string.Empty)}"; - mergeSql.AppendLine(line); + mergeSql.AppendLine($"({string.Join(", ", values)}){(i < entities.Count - 1 ? "," : string.Empty)}"); } mergeSql.AppendLine($") AS Source ({string.Join(", ", columnNames)})"); @@ -145,6 +123,10 @@ public static class BulkUpsertExtensions return (mergeSql.ToString(), parameters.ToArray()); } + // ------------------------------------------------------------------------- + // SQLite + // ------------------------------------------------------------------------- + private static (string, object[]) GenerateSqliteUpsert( DbContext dbContext, IList entities, @@ -157,9 +139,7 @@ public static class BulkUpsertExtensions var props = entityType.GetProperties().ToList(); var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject); - var columnNames = props - .Select(p => p.GetColumnName(storeObject)!) - .ToList(); + var columnNames = props.Select(p => p.GetColumnName(storeObject)!).ToList(); var sb = new StringBuilder(); var parameters = new List(); @@ -175,36 +155,30 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; - var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); - var converter = property.GetTypeMapping().Converter; - if (converter != null) - value = converter.ConvertToProvider(value); - + if (converter != null) value = converter.ConvertToProvider(value); placeholders.Add(paramName); parameters.Add(value!); } sb.Append($"({string.Join(", ", placeholders)})"); - if (i < entities.Count - 1) - sb.Append(", "); + if (i < entities.Count - 1) sb.Append(", "); } sb.AppendLine(); sb.AppendLine($"ON CONFLICT(\"{keyColumnName}\") DO UPDATE SET"); - - var updateAssignments = columnNames - .Where(c => c != keyColumnName) - .Select(c => $"\"{c}\"=excluded.\"{c}\""); - - sb.AppendLine(string.Join(", ", updateAssignments) + ";"); + sb.AppendLine(string.Join(", ", columnNames.Where(c => c != keyColumnName).Select(c => $"\"{c}\"=excluded.\"{c}\"")) + ";"); return (sb.ToString(), parameters.ToArray()); } + // ------------------------------------------------------------------------- + // PostgreSQL + // ------------------------------------------------------------------------- + private static (string, object[]) GeneratePostgresUpsert( DbContext dbContext, IList entities, @@ -214,14 +188,10 @@ public static class BulkUpsertExtensions var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!; var tableName = entityType.GetTableName(); var storeObject = StoreObjectIdentifier.Table(tableName!, entityType.GetSchema()); - var props = entityType.GetProperties().ToList(); - var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject); - var columnNames = props - .Select(p => p.GetColumnName(storeObject)!) - .ToList(); + var columnNames = props.Select(p => p.GetColumnName(storeObject)!).ToList(); var sb = new StringBuilder(); var parameters = new List(); @@ -237,16 +207,12 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; - var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); - var converter = property.GetTypeMapping().Converter; - if (converter != null) - value = converter.ConvertToProvider(value); + if (converter != null) value = converter.ConvertToProvider(value); - // Detect json/jsonb column types and cast the parameter so PostgreSQL accepts it. var columnType = property.GetColumnType(); if (columnType.StartsWith("jsonb", StringComparison.OrdinalIgnoreCase)) placeholders.Add($"CAST({paramName} AS jsonb)"); @@ -254,27 +220,25 @@ public static class BulkUpsertExtensions placeholders.Add($"CAST({paramName} AS json)"); else placeholders.Add(paramName); - + parameters.Add(value!); } sb.Append($"({string.Join(", ", placeholders)})"); - if (i < entities.Count - 1) - sb.Append(", "); + if (i < entities.Count - 1) sb.Append(", "); } sb.AppendLine(); sb.AppendLine($"ON CONFLICT (\"{keyColumnName}\") DO UPDATE SET"); - - var updateAssignments = columnNames - .Where(c => c != keyColumnName) - .Select(c => $"\"{c}\" = EXCLUDED.\"{c}\""); - - sb.AppendLine(string.Join(", ", updateAssignments) + ";"); + sb.AppendLine(string.Join(", ", columnNames.Where(c => c != keyColumnName).Select(c => $"\"{c}\" = EXCLUDED.\"{c}\"")) + ";"); return (sb.ToString(), parameters.ToArray()); } + // ------------------------------------------------------------------------- + // MySQL + // ------------------------------------------------------------------------- + private static (string, object[]) GenerateMySqlUpsert( DbContext dbContext, IList entities, @@ -284,14 +248,10 @@ public static class BulkUpsertExtensions var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!; var tableName = entityType.GetTableName(); var storeObject = StoreObjectIdentifier.Table(tableName!, entityType.GetSchema()); - var props = entityType.GetProperties().ToList(); - var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject); - var columnNames = props - .Select(p => p.GetColumnName(storeObject)!) - .ToList(); + var columnNames = props.Select(p => p.GetColumnName(storeObject)!).ToList(); var sb = new StringBuilder(); var parameters = new List(); @@ -307,36 +267,30 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; - var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); - var converter = property.GetTypeMapping().Converter; - if (converter != null) - value = converter.ConvertToProvider(value); - + if (converter != null) value = converter.ConvertToProvider(value); placeholders.Add(paramName); parameters.Add(value!); } sb.Append($"({string.Join(", ", placeholders)})"); - if (i < entities.Count - 1) - sb.Append(", "); + if (i < entities.Count - 1) sb.Append(", "); } sb.AppendLine(); sb.AppendLine("ON DUPLICATE KEY UPDATE"); - - var updateAssignments = columnNames - .Where(c => c != keyColumnName) - .Select(c => $"`{c}` = VALUES(`{c}`)"); - - sb.AppendLine(string.Join(", ", updateAssignments) + ";"); + sb.AppendLine(string.Join(", ", columnNames.Where(c => c != keyColumnName).Select(c => $"`{c}` = VALUES(`{c}`)")) + ";"); return (sb.ToString(), parameters.ToArray()); } + // ------------------------------------------------------------------------- + // Oracle + // ------------------------------------------------------------------------- + private static (string, object[]) GenerateOracleUpsert( DbContext dbContext, IList entities, @@ -345,18 +299,24 @@ public static class BulkUpsertExtensions { var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!; var schema = entityType.GetSchema(); - var tableName = entityType.GetTableName(); - var storeObject = StoreObjectIdentifier.Table(tableName!, schema); - var fullName = !string.IsNullOrEmpty(schema) ? $"{schema}.{tableName}" : tableName; + var tableName = entityType.GetTableName()!; + var storeObject = StoreObjectIdentifier.Table(tableName, schema); + + // Both schema and table must be quoted so Oracle treats them as + // case-sensitive identifiers, matching what EF Core migrations create. + var fullName = !string.IsNullOrEmpty(schema) + ? $"\"{schema}\".\"{tableName}\"" + : $"\"{tableName}\""; var props = entityType.GetProperties().ToList(); - var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; - var keyColumnName = keyProp.GetColumnName(storeObject); + var keyColumnName = keyProp.GetColumnName(storeObject)!; - var columnNames = props - .Select(p => p.GetColumnName(storeObject)!) + // Pre-build quoted column name list once; reuse throughout. + var quotedColumnNames = props + .Select(p => $"\"{p.GetColumnName(storeObject)}\"") .ToList(); + var quotedKeyColumnName = $"\"{keyColumnName}\""; var sb = new StringBuilder(); var parameters = new List(); @@ -384,28 +344,61 @@ public static class BulkUpsertExtensions parameters.Add(value!); - // Oracle aliases must match the column name - var alias = property.GetColumnName(storeObject); - lineParts.Add($"{paramName} AS {alias}"); + // Alias must be quoted so Oracle preserves case, matching the + // quoted references in the WHEN MATCHED / WHEN NOT MATCHED clauses. + var quotedAlias = $"\"{property.GetColumnName(storeObject)}\""; + + // Oracle cannot infer the bind parameter type from a bare SELECT … + // FROM DUAL — there is no target column to derive it from. For + // NVARCHAR2 columns this causes ODP.NET to default to VARCHAR2, + // which leads to datatype mismatch errors in the MERGE. An explicit + // CAST restores the correct type. The length is read from the EF + // column type string (e.g. "NVARCHAR2(500)") so it matches the + // actual column definition rather than an arbitrary hardcoded value. + string expr; + var columnType = property.GetColumnType() ?? string.Empty; + if (columnType.StartsWith("NVARCHAR2", StringComparison.OrdinalIgnoreCase)) + { + var length = ParseNVarchar2Length(columnType); + expr = $"CAST({paramName} AS NVARCHAR2({length}))"; + } + else + { + expr = paramName; + } + + lineParts.Add($"{expr} AS {quotedAlias}"); } - // Comma if not last - var suffix = (i < entities.Count - 1) ? " FROM DUAL UNION ALL SELECT" : " FROM DUAL"; + var suffix = i < entities.Count - 1 ? " FROM DUAL UNION ALL SELECT" : " FROM DUAL"; sb.AppendLine(string.Join(", ", lineParts) + suffix); } - sb.AppendLine($") Source ON (Target.{keyColumnName} = Source.{keyColumnName})"); + sb.AppendLine($") Source ON (Target.{quotedKeyColumnName} = Source.{quotedKeyColumnName})"); sb.AppendLine("WHEN MATCHED THEN UPDATE SET"); - - var updateSetClauses = columnNames - .Where(c => c != keyColumnName) - .Select(c => $"Target.{c} = Source.{c}"); - - sb.AppendLine(string.Join(", ", updateSetClauses)); + sb.AppendLine(string.Join(", ", quotedColumnNames + .Where(c => c != quotedKeyColumnName) + .Select(c => $"Target.{c} = Source.{c}"))); sb.AppendLine("WHEN NOT MATCHED THEN"); - sb.AppendLine($"INSERT ({string.Join(", ", columnNames)})"); - sb.AppendLine($"VALUES ({string.Join(", ", columnNames.Select(c => $"Source.{c}"))});"); + sb.AppendLine($"INSERT ({string.Join(", ", quotedColumnNames)})"); + sb.AppendLine($"VALUES ({string.Join(", ", quotedColumnNames.Select(c => $"Source.{c}"))});"); return (sb.ToString(), parameters.ToArray()); } -} \ No newline at end of file + + /// + /// Extracts the maximum length from an Oracle NVARCHAR2 column type string. + /// For example, "NVARCHAR2(500)" returns 500. + /// Falls back to 2000 (Oracle's maximum for NVARCHAR2) if the string is malformed. + /// + private static int ParseNVarchar2Length(string columnType) + { + var open = columnType.IndexOf('('); + var close = columnType.IndexOf(')'); + if (open >= 0 && close > open && + int.TryParse(columnType.AsSpan(open + 1, close - open - 1), out var length)) + return length; + + return 2000; + } +} From 6b7296fa29f9a439e908863bcc247fb94ec23219 Mon Sep 17 00:00:00 2001 From: MohitGuptaC Date: Wed, 24 Jun 2026 10:24:05 +0530 Subject: [PATCH 22/33] Fix Comments and Casting logic of NVARCHAR2 --- .../Extensions/BulkUpsertExtensions.cs | 198 ++++++++++-------- 1 file changed, 111 insertions(+), 87 deletions(-) diff --git a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs index 2749790bd..707702589 100644 --- a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs +++ b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs @@ -16,6 +16,12 @@ public static class BulkUpsertExtensions /// /// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector. /// + /// The type of the database context. + /// The type of the entity being upserted. + /// The database context where the bulk upsert operation will be executed. + /// The list of entities to be upserted. + /// An expression used to determine the key for upsert operations. + /// A token to observe while waiting for the operation to complete. public static async Task BulkUpsertAsync( this TDbContext dbContext, IList entities, @@ -30,6 +36,13 @@ public static class BulkUpsertExtensions /// /// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector and optional batch size. /// + /// The type of the database context. + /// The type of the entity being upserted. + /// The database context where the bulk upsert operation will be executed. + /// The list of entities to be upserted. + /// An expression used to determine the key for upsert operations. + /// The size of each batch for processing the upsert operation. Defaults to 50. + /// A token to observe while waiting for the operation to complete. /// Thrown if the database provider for the context is not supported. public static async Task BulkUpsertAsync( this TDbContext dbContext, @@ -43,29 +56,30 @@ public static class BulkUpsertExtensions if (entities.Count == 0) return; + // Identify the current provider (e.g., "Microsoft.EntityFrameworkCore.SqlServer") var providerName = dbContext.Database.ProviderName?.ToLowerInvariant() ?? string.Empty; + // Determine the method for generating SQL based on the provider Func, Expression>, (string, object[])> generateSql = providerName switch { var pn when pn.Contains("sqlserver") => GenerateSqlServerUpsert, - var pn when pn.Contains("sqlite") => GenerateSqliteUpsert, - var pn when pn.Contains("postgres") => GeneratePostgresUpsert, - var pn when pn.Contains("mysql") => GenerateMySqlUpsert, - var pn when pn.Contains("oracle") => GenerateOracleUpsert, + var pn when pn.Contains("sqlite") => GenerateSqliteUpsert, + var pn when pn.Contains("postgres") => GeneratePostgresUpsert, + var pn when pn.Contains("mysql") => GenerateMySqlUpsert, + var pn when pn.Contains("oracle") => GenerateOracleUpsert, _ => throw new NotSupportedException($"Provider '{providerName}' is not supported.") }; + // Loop through batched entities foreach (var batch in entities.Chunk(batchSize)) { + // Generate SQL and parameters var (sql, parameters) = generateSql(dbContext, batch, keySelector); + await dbContext.Database.ExecuteSqlRawAsync(sql, parameters, cancellationToken); } } - // ------------------------------------------------------------------------- - // SQL Server - // ------------------------------------------------------------------------- - private static (string, object[]) GenerateSqlServerUpsert( DbContext dbContext, IList entities, @@ -78,7 +92,9 @@ public static class BulkUpsertExtensions var props = entityType.GetProperties().ToList(); var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = $"[{keyProp.GetColumnName(storeObject)}]"; - var columnNames = props.Select(p => $"[{p.GetColumnName(storeObject)}]").ToList(); + var columnNames = props + .Select(p => $"[{p.GetColumnName(storeObject)}]") + .ToList(); var mergeSql = new StringBuilder(); mergeSql.AppendLine($"MERGE {tableName} AS Target"); @@ -95,21 +111,27 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; + + // If it's a shadow property, retrieve value via Entry(..).Property(..) var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); - var converter = property.GetTypeMapping().Converter; - if (converter != null) value = converter.ConvertToProvider(value)!; + var converter = property.GetTypeMapping().Converter; + if (converter != null) + value = converter.ConvertToProvider(value)!; + + // Explicitly cast null values for varbinary columns if (property.GetColumnType().StartsWith("varbinary", StringComparison.OrdinalIgnoreCase) && value is null) - values.Add("CAST(NULL AS varbinary(max))"); + values.Add("CAST(NULL AS varbinary(max))"); // Explicitly cast null else values.Add(paramName); - + parameters.Add(value!); } - mergeSql.AppendLine($"({string.Join(", ", values)}){(i < entities.Count - 1 ? "," : string.Empty)}"); + var line = $"({string.Join(", ", values)}){(i < entities.Count - 1 ? "," : string.Empty)}"; + mergeSql.AppendLine(line); } mergeSql.AppendLine($") AS Source ({string.Join(", ", columnNames)})"); @@ -123,10 +145,6 @@ public static class BulkUpsertExtensions return (mergeSql.ToString(), parameters.ToArray()); } - // ------------------------------------------------------------------------- - // SQLite - // ------------------------------------------------------------------------- - private static (string, object[]) GenerateSqliteUpsert( DbContext dbContext, IList entities, @@ -139,7 +157,9 @@ public static class BulkUpsertExtensions var props = entityType.GetProperties().ToList(); var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject); - var columnNames = props.Select(p => p.GetColumnName(storeObject)!).ToList(); + var columnNames = props + .Select(p => p.GetColumnName(storeObject)!) + .ToList(); var sb = new StringBuilder(); var parameters = new List(); @@ -155,30 +175,36 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; + var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); + var converter = property.GetTypeMapping().Converter; - if (converter != null) value = converter.ConvertToProvider(value); + if (converter != null) + value = converter.ConvertToProvider(value); + placeholders.Add(paramName); parameters.Add(value!); } sb.Append($"({string.Join(", ", placeholders)})"); - if (i < entities.Count - 1) sb.Append(", "); + if (i < entities.Count - 1) + sb.Append(", "); } sb.AppendLine(); sb.AppendLine($"ON CONFLICT(\"{keyColumnName}\") DO UPDATE SET"); - sb.AppendLine(string.Join(", ", columnNames.Where(c => c != keyColumnName).Select(c => $"\"{c}\"=excluded.\"{c}\"")) + ";"); + + var updateAssignments = columnNames + .Where(c => c != keyColumnName) + .Select(c => $"\"{c}\"=excluded.\"{c}\""); + + sb.AppendLine(string.Join(", ", updateAssignments) + ";"); return (sb.ToString(), parameters.ToArray()); } - // ------------------------------------------------------------------------- - // PostgreSQL - // ------------------------------------------------------------------------- - private static (string, object[]) GeneratePostgresUpsert( DbContext dbContext, IList entities, @@ -188,10 +214,14 @@ public static class BulkUpsertExtensions var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!; var tableName = entityType.GetTableName(); var storeObject = StoreObjectIdentifier.Table(tableName!, entityType.GetSchema()); + var props = entityType.GetProperties().ToList(); + var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject); - var columnNames = props.Select(p => p.GetColumnName(storeObject)!).ToList(); + var columnNames = props + .Select(p => p.GetColumnName(storeObject)!) + .ToList(); var sb = new StringBuilder(); var parameters = new List(); @@ -207,12 +237,16 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; + var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); - var converter = property.GetTypeMapping().Converter; - if (converter != null) value = converter.ConvertToProvider(value); + var converter = property.GetTypeMapping().Converter; + if (converter != null) + value = converter.ConvertToProvider(value); + + // Detect json/jsonb column types and cast the parameter so PostgreSQL accepts it. var columnType = property.GetColumnType(); if (columnType.StartsWith("jsonb", StringComparison.OrdinalIgnoreCase)) placeholders.Add($"CAST({paramName} AS jsonb)"); @@ -220,25 +254,27 @@ public static class BulkUpsertExtensions placeholders.Add($"CAST({paramName} AS json)"); else placeholders.Add(paramName); - + parameters.Add(value!); } sb.Append($"({string.Join(", ", placeholders)})"); - if (i < entities.Count - 1) sb.Append(", "); + if (i < entities.Count - 1) + sb.Append(", "); } sb.AppendLine(); sb.AppendLine($"ON CONFLICT (\"{keyColumnName}\") DO UPDATE SET"); - sb.AppendLine(string.Join(", ", columnNames.Where(c => c != keyColumnName).Select(c => $"\"{c}\" = EXCLUDED.\"{c}\"")) + ";"); + + var updateAssignments = columnNames + .Where(c => c != keyColumnName) + .Select(c => $"\"{c}\" = EXCLUDED.\"{c}\""); + + sb.AppendLine(string.Join(", ", updateAssignments) + ";"); return (sb.ToString(), parameters.ToArray()); } - // ------------------------------------------------------------------------- - // MySQL - // ------------------------------------------------------------------------- - private static (string, object[]) GenerateMySqlUpsert( DbContext dbContext, IList entities, @@ -248,10 +284,14 @@ public static class BulkUpsertExtensions var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!; var tableName = entityType.GetTableName(); var storeObject = StoreObjectIdentifier.Table(tableName!, entityType.GetSchema()); + var props = entityType.GetProperties().ToList(); + var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject); - var columnNames = props.Select(p => p.GetColumnName(storeObject)!).ToList(); + var columnNames = props + .Select(p => p.GetColumnName(storeObject)!) + .ToList(); var sb = new StringBuilder(); var parameters = new List(); @@ -267,30 +307,36 @@ public static class BulkUpsertExtensions foreach (var property in props) { var paramName = $"{{{parameterCount++}}}"; + var value = property.IsShadowProperty() ? dbContext.Entry(entity).Property(property.Name).CurrentValue : property.PropertyInfo?.GetValue(entity); + var converter = property.GetTypeMapping().Converter; - if (converter != null) value = converter.ConvertToProvider(value); + if (converter != null) + value = converter.ConvertToProvider(value); + placeholders.Add(paramName); parameters.Add(value!); } sb.Append($"({string.Join(", ", placeholders)})"); - if (i < entities.Count - 1) sb.Append(", "); + if (i < entities.Count - 1) + sb.Append(", "); } sb.AppendLine(); sb.AppendLine("ON DUPLICATE KEY UPDATE"); - sb.AppendLine(string.Join(", ", columnNames.Where(c => c != keyColumnName).Select(c => $"`{c}` = VALUES(`{c}`)")) + ";"); + + var updateAssignments = columnNames + .Where(c => c != keyColumnName) + .Select(c => $"`{c}` = VALUES(`{c}`)"); + + sb.AppendLine(string.Join(", ", updateAssignments) + ";"); return (sb.ToString(), parameters.ToArray()); } - // ------------------------------------------------------------------------- - // Oracle - // ------------------------------------------------------------------------- - private static (string, object[]) GenerateOracleUpsert( DbContext dbContext, IList entities, @@ -301,18 +347,19 @@ public static class BulkUpsertExtensions var schema = entityType.GetSchema(); var tableName = entityType.GetTableName()!; var storeObject = StoreObjectIdentifier.Table(tableName, schema); - - // Both schema and table must be quoted so Oracle treats them as - // case-sensitive identifiers, matching what EF Core migrations create. + + // Both schema and table must be quoted so Oracle treats them as case-sensitive + // identifiers, matching what EF Core migrations create. var fullName = !string.IsNullOrEmpty(schema) ? $"\"{schema}\".\"{tableName}\"" : $"\"{tableName}\""; var props = entityType.GetProperties().ToList(); + var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!; var keyColumnName = keyProp.GetColumnName(storeObject)!; - // Pre-build quoted column name list once; reuse throughout. + // Pre-build quoted column names once and reuse throughout all clauses. var quotedColumnNames = props .Select(p => $"\"{p.GetColumnName(storeObject)}\"") .ToList(); @@ -343,30 +390,23 @@ public static class BulkUpsertExtensions value = converter.ConvertToProvider(value); parameters.Add(value!); - - // Alias must be quoted so Oracle preserves case, matching the - // quoted references in the WHEN MATCHED / WHEN NOT MATCHED clauses. + + // Aliases must be quoted so Oracle preserves their case, matching + // the quoted references in ON, UPDATE SET, and INSERT/VALUES below. var quotedAlias = $"\"{property.GetColumnName(storeObject)}\""; - - // Oracle cannot infer the bind parameter type from a bare SELECT … - // FROM DUAL — there is no target column to derive it from. For - // NVARCHAR2 columns this causes ODP.NET to default to VARCHAR2, - // which leads to datatype mismatch errors in the MERGE. An explicit - // CAST restores the correct type. The length is read from the EF - // column type string (e.g. "NVARCHAR2(500)") so it matches the - // actual column definition rather than an arbitrary hardcoded value. - string expr; + + // In a SELECT … FROM DUAL subquery, ODP.NET has no target column to + // derive bind parameter types from and defaults to VARCHAR2 for .NET + // strings. Elsa's Oracle migrations define string columns as NVARCHAR2, + // so an explicit CAST is required to avoid a datatype mismatch error. + // The full EF Core column type string (e.g. "NVARCHAR2(450 CHAR)") is + // used directly in the CAST so all Oracle type variants are handled + // correctly without any string parsing. var columnType = property.GetColumnType() ?? string.Empty; - if (columnType.StartsWith("NVARCHAR2", StringComparison.OrdinalIgnoreCase)) - { - var length = ParseNVarchar2Length(columnType); - expr = $"CAST({paramName} AS NVARCHAR2({length}))"; - } - else - { - expr = paramName; - } - + var expr = columnType.StartsWith("NVARCHAR2", StringComparison.OrdinalIgnoreCase) + ? $"CAST({paramName} AS {columnType})" + : paramName; + lineParts.Add($"{expr} AS {quotedAlias}"); } @@ -385,20 +425,4 @@ public static class BulkUpsertExtensions return (sb.ToString(), parameters.ToArray()); } - - /// - /// Extracts the maximum length from an Oracle NVARCHAR2 column type string. - /// For example, "NVARCHAR2(500)" returns 500. - /// Falls back to 2000 (Oracle's maximum for NVARCHAR2) if the string is malformed. - /// - private static int ParseNVarchar2Length(string columnType) - { - var open = columnType.IndexOf('('); - var close = columnType.IndexOf(')'); - if (open >= 0 && close > open && - int.TryParse(columnType.AsSpan(open + 1, close - open - 1), out var length)) - return length; - - return 2000; - } -} +} \ No newline at end of file From 5336f9ea060715c98114ba95ab59b68f820e1aa0 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 27 Jun 2026 04:04:39 +0200 Subject: [PATCH 23/33] Increase AI host test coverage --- .../AIToolRegistryTests.cs | 19 ++++++ .../Context/AIContextResolverTests.cs | 60 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs b/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs index d580a3402..ebac45da3 100644 --- a/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs +++ b/test/unit/Elsa.AI.Host.UnitTests/AIToolRegistryTests.cs @@ -253,6 +253,25 @@ public class AIToolRegistryTests Assert.True(enablement.IsEnabled(definition)); } + [Fact(DisplayName = "Tool enablement disables administrative tools by name")] + public void ToolEnablementDisablesAdministrativeToolsByName() + { + var enablement = new AIToolEnablementService(); + var definition = new AIToolDefinition + { + Name = "admin", + DisplayName = "Admin", + Mutability = AIToolMutability.Administrative + }; + + enablement.EnableAdministrative("ADMIN"); + Assert.True(enablement.IsEnabled(definition)); + + enablement.Disable("admin"); + + Assert.False(enablement.IsEnabled(definition)); + } + [Fact(DisplayName = "Tool registry filters agent-scoped tools by agent")] public async Task ToolRegistryFiltersAgentScopedToolsByAgent() { diff --git a/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs b/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs index c9ac0aadb..90b2f7902 100644 --- a/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs +++ b/test/unit/Elsa.AI.Host.UnitTests/Context/AIContextResolverTests.cs @@ -59,6 +59,44 @@ public class AIContextResolverTests Assert.Equal("visible", context.Data["displayName"]!.GetValue()); } + [Fact(DisplayName = "Context resolver redacts nested context payloads")] + public async Task ContextResolverRedactsNestedContextPayloads() + { + using var provider = CreateProvider(services => services.AddSingleton()); + var resolver = provider.GetRequiredService(); + + var result = await resolver.ResolveAsync(new AIChatRequest + { + UserId = "user-1", + Attachments = [new AIContextAttachment { Kind = "NestedSensitive" }] + }); + + var context = Assert.Single(result); + var profile = Assert.IsType(context.Data["profile"]); + var history = Assert.IsType(context.Data["history"]); + + Assert.Equal("[redacted]", profile["password"]!.GetValue()); + Assert.Equal("visible", profile["displayName"]!.GetValue()); + Assert.Equal("[redacted]", history[0]!.GetValue()); + Assert.Equal(42, history[1]!.GetValue()); + Assert.True(history[2]!.GetValue()); + } + + [Fact(DisplayName = "Context resolver ignores attachments without providers")] + public async Task ContextResolverIgnoresAttachmentsWithoutProviders() + { + using var provider = CreateProvider(_ => { }); + var resolver = provider.GetRequiredService(); + + var result = await resolver.ResolveAsync(new AIChatRequest + { + UserId = "user-1", + Attachments = [new AIContextAttachment { Kind = "Unknown" }] + }); + + Assert.Empty(result); + } + [Fact(DisplayName = "Context resolver uses the last provider for duplicate provider kinds")] public async Task ContextResolverUsesTheLastProviderForDuplicateProviderKinds() { @@ -148,6 +186,28 @@ public class AIContextResolverTests } } + private class NestedSensitiveContextProvider : IAIContextProvider + { + public string Kind => "NestedSensitive"; + + public ValueTask ResolveAsync(AIContextResolutionRequest request, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult(new AIResolvedContext + { + Kind = Kind, + Data = new JsonObject + { + ["profile"] = new JsonObject + { + ["password"] = "secret-value", + ["displayName"] = "visible" + }, + ["history"] = new JsonArray("Bearer abcdefgh", 42, true) + } + }); + } + } + private class DuplicateContextProvider(string summary, string kind = "Duplicate") : IAIContextProvider { public string Kind => kind; From 4f8957857277bf824c95a2525387547f01db0df9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 1 Jul 2026 00:06:01 +0200 Subject: [PATCH 24/33] Refresh roadmap from current Elsa evidence --- ROADMAP.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 385dde1f9..480011a6f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Elsa Roadmap -Last refreshed: 2026-06-10 +Last refreshed: 2026-07-01 This roadmap is a product direction document, not a fixed release calendar. Elsa is developed through a mix of core maintainer work, customer-funded work, and community contributions, so sequencing can change when real-world demand changes. The intent is stable: make Elsa the most productive, dependable, and extensible workflow platform for the .NET ecosystem. @@ -93,24 +93,24 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio These are already present in the codebase and should be treated as foundations for the next roadmap slices: - Multi-targeting for `net8.0`, `net9.0`, and `net10.0` in [`src/Directory.Build.props`](src/Directory.Build.props). -- The `3.7.0` release train shipped across [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0), [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.0), and [Extensions](https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.0) in May 2026, promoting shell integration, Studio authentication, workflow diagnostics, and extension package metadata into released foundations. The [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.8.0-preview1) and [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.8.0-preview1) `3.8.0-preview1` releases on June 1, 2026 then added the next preview slice of graceful shutdown, richer diagnostics, secrets, and newer designer surfaces. +- The `3.7.0` release train shipped across [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0), [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.0), and [Extensions](https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.0) in May 2026, promoting shell integration, Studio authentication, workflow diagnostics, and extension package metadata into released foundations. The [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.8.0-preview1) and [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.8.0-preview1) `3.8.0-preview1` releases on June 1, 2026 then added the next preview slice of graceful shutdown, richer diagnostics, secrets, and newer designer surfaces. The `3.7.1` patch train then shipped across [Core](https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1), [Studio](https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.1), and [Extensions](https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.1) on June 21, 2026, tightening Azure Service Bus startup reliability, aligning Studio with the released Core API client, and hardening Quartz durability and endpoint-name pressure in Extensions. - Modular core packages under [`src/modules`](src/modules), with code-first features and CShells shell features documented in [`doc/wiki/module-system.md`](doc/wiki/module-system.md). - A modular server host using CShells and Nuplane package loading in [`src/apps/Elsa.ModularServer.Web`](src/apps/Elsa.ModularServer.Web). - Runtime admin, quiescence, drain, and interrupted recovery infrastructure in [`Elsa.Workflows.Runtime`](src/modules/Elsa.Workflows.Runtime) and runtime admin endpoints in [`Elsa.Workflows.Api`](src/modules/Elsa.Workflows.Api/Endpoints/RuntimeAdmin). - Distributed runtime support in [`Elsa.Workflows.Runtime.Distributed`](src/modules/Elsa.Workflows.Runtime.Distributed). - Structured diagnostics with recent/live capture plus SQLite persistence in [`Elsa.Diagnostics.StructuredLogs`](src/modules/Elsa.Diagnostics.StructuredLogs) and [`Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite`](src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite). - Raw stdout/stderr console diagnostics in [`Elsa.Diagnostics.ConsoleLogs`](src/modules/Elsa.Diagnostics.ConsoleLogs), with the post-3.7 console pipeline now carrying workflow and activity execution context through [PR #7536](https://github.com/elsa-workflows/elsa-core/pull/7536). -- Core OpenTelemetry diagnostics are actively in productization through [PR #7537](https://github.com/elsa-workflows/elsa-core/pull/7537), which adds OTLP ingestion, bounded storage, REST APIs, SignalR live updates, collector configuration, security checks, and tests. -- Core `main` now includes `Elsa.AI.Abstractions`, `Elsa.AI.Host`, `Elsa.AI.Copilot`, and `Elsa.AI.Persistence.EFCore` through [PR #7523](https://github.com/elsa-workflows/elsa-core/pull/7523), giving Weaver a merged server-side foundation while Studio UX and broader productization remain roadmap work. +- Core `main` now includes `Elsa.Diagnostics.OpenTelemetry`, which provides OTLP ingestion, bounded in-memory storage, REST APIs, SignalR live updates, collector configuration, permissions, and tests in [`src/modules/Elsa.Diagnostics.OpenTelemetry`](src/modules/Elsa.Diagnostics.OpenTelemetry). The productization gap is no longer "does a backend exist?" but rather release packaging, default workflow semantic metrics, and diagnostics correlation. +- Core `main` now includes `Elsa.AI.Abstractions`, `Elsa.AI.Host`, `Elsa.AI.Copilot`, and `Elsa.AI.Persistence.EFCore` through [PR #7523](https://github.com/elsa-workflows/elsa-core/pull/7523), and Studio `main` now includes `Elsa.Studio.AI` through [elsa-studio#900](https://github.com/elsa-workflows/elsa-studio/pull/900). That gives Weaver both server and Studio workspace foundations, while proposal actions, broader authoring contracts, and polished product UX remain roadmap work. - State machine core activity support in [`Elsa.Workflows.Core/Activities/StateMachine`](src/modules/Elsa.Workflows.Core/Activities/StateMachine). - ElsaScript DSL and blob storage integration in [`Elsa.Dsl.ElsaScript`](src/modules/Elsa.Dsl.ElsaScript) and [`Elsa.WorkflowProviders.BlobStorage.ElsaScript`](src/modules/Elsa.WorkflowProviders.BlobStorage.ElsaScript). - Activity unit testing helpers and guidance in [`src/common/Elsa.Testing.Shared`](src/common/Elsa.Testing.Shared) and [`doc/qa/test-guidelines.md`](doc/qa/test-guidelines.md). -- Label infrastructure in [`Elsa.Labels`](src/modules/Elsa.Labels), which is the likely backend foundation for workflow categories, tags, and folders. +- Label infrastructure now spans Core and Studio: [`Elsa.Labels`](src/modules/Elsa.Labels) exposes label and workflow-label endpoints, and [`Elsa.Studio.Labels`](https://github.com/elsa-workflows/elsa-studio/tree/main/src/modules/Elsa.Studio.Labels) adds label management pages plus workflow-definition label editing. Folder views, broader metadata search, and richer organization UX remain roadmap work. - Elsa Studio is already a modular Blazor product shell with workflow authoring, instance browsing, designer modules, diagnostics, authentication, localization, branding, custom elements, and early React wrapper work in [elsa-workflows/elsa-studio](https://github.com/elsa-workflows/elsa-studio). - Studio `3.7.0` shipped the modern authentication framework, Elsa Identity and OIDC modules, activity call-stack visualization, incident count badges, pending-instance filtering, and custom theme/DataPanel extensibility. - Studio `3.8.0-preview1` shipped the server logs module, console logs module, structured-log storage diagnostics, the OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), sequence and state-machine designer foundations, the secrets module, and the alterations designer. - Elsa Extensions is an active modular integration repository with 70+ module projects in [elsa-workflows/elsa-extensions](https://github.com/elsa-workflows/elsa-extensions), targeting `net8.0`, `net9.0`, and `net10.0`. -- Extensions already provide broad integration foundations: Connections, Secrets, Agents, OpenAPI, SQL/CSV/data tooling, messaging, schedulers, cloud storage, logging, webhooks, persistence providers, and external system activities. +- Extensions already provide broad integration foundations: Connections, Secrets, Agents, OpenAPI, SQL/CSV/data tooling, messaging, schedulers, cloud storage, logging, webhooks, persistence providers, LDAP, and external system activities. - Extensions `3.7.0` adds package manifest metadata, infrastructure attributes, shell features for MassTransit/Quartz/Webhooks, Dapper and MongoDB activity execution-chain lookups, Dapper bookmark queue filtering, Kafka multitenancy/schema-trigger work, Quartz lifecycle/job cleanup fixes, and other operational hardening. The public roadmap issue remains useful history: [elsa-workflows/elsa-core#3232](https://github.com/elsa-workflows/elsa-core/issues/3232). Several items in that issue are now done in code but still open in the issue body, so this file should be considered the current working roadmap. @@ -124,7 +124,7 @@ High-value items: - Complete the graceful-shutdown operational slice: back-pressure-aware bookmark queueing, health checks, pause persistence across reactivation, and contract tests. The remaining task list is visible in [`specs/002-graceful-shutdown/tasks.md`](specs/002-graceful-shutdown/tasks.md). - Close the workflow recovery story around interrupted, crashed, and stuck-running instances. This directly addresses [#4833](https://github.com/elsa-workflows/elsa-core/issues/4833) and should include Studio-facing recovery states, operator actions, and clear audit records. - Harden distributed execution semantics: child workflow completion, bookmark races, duplicate dispatch, timer/delay behavior, and clustered refresh/reload. Community signal shows this repeatedly in [discussion #5857](https://github.com/elsa-workflows/elsa-core/discussions/5857), [#7397](https://github.com/elsa-workflows/elsa-core/issues/7397), [#7405](https://github.com/elsa-workflows/elsa-core/issues/7405), and related FlowJoin/bookmark issues. -- Treat scheduler and messaging correctness as release-blocking infrastructure. Extensions issues around Quartz clustering and recovery ([elsa-extensions#109](https://github.com/elsa-workflows/elsa-extensions/issues/109), [elsa-extensions#101](https://github.com/elsa-workflows/elsa-extensions/issues/101)), Hangfire duplicate jobs ([elsa-extensions#121](https://github.com/elsa-workflows/elsa-extensions/issues/121)), MassTransit stimulus routing ([elsa-extensions#72](https://github.com/elsa-workflows/elsa-extensions/issues/72)), and Kafka extensibility ([elsa-extensions#134](https://github.com/elsa-workflows/elsa-extensions/issues/134)) all point to the same production theme: clustered workload behavior must be boring, observable, and customizable. +- Treat scheduler and messaging correctness as release-blocking infrastructure. The new `3.7.1` patch line improved Azure Service Bus startup behavior and stable instance naming in Core ([#7732](https://github.com/elsa-workflows/elsa-core/issues/7732), [#7736](https://github.com/elsa-workflows/elsa-core/issues/7736), [#7742](https://github.com/elsa-workflows/elsa-core/pull/7742)) and tightened Quartz durable trigger scheduling in Extensions ([elsa-extensions#162](https://github.com/elsa-workflows/elsa-extensions/pull/162)). That progress is useful, but active issues around Quartz clustering and recovery ([elsa-extensions#109](https://github.com/elsa-workflows/elsa-extensions/issues/109), [elsa-extensions#101](https://github.com/elsa-workflows/elsa-extensions/issues/101)), Hangfire duplicate jobs ([elsa-extensions#121](https://github.com/elsa-workflows/elsa-extensions/issues/121)), MassTransit stimulus routing ([elsa-extensions#72](https://github.com/elsa-workflows/elsa-extensions/issues/72)), Kafka extensibility ([elsa-extensions#134](https://github.com/elsa-workflows/elsa-extensions/issues/134)), and Azure Service Bus backlog/startup pressure ([#7735](https://github.com/elsa-workflows/elsa-core/issues/7735), [#7737](https://github.com/elsa-workflows/elsa-core/issues/7737)) all point to the same production theme: clustered workload behavior must be boring, observable, and customizable. - Turn the draft native background execution architecture into an implementation plan. [#7356](https://github.com/elsa-workflows/elsa-core/issues/7356) and [#7313](https://github.com/elsa-workflows/elsa-core/issues/7313) point toward an engine-owned, workflow-aware runtime that can evolve toward an actor-model abstraction without coupling Elsa to Orleans, Proto.Actor, or any single backend. - Treat persistence and migration reliability as a product feature: provider-specific migration validation, large-tenant performance tests, safer defaults, and upgrade notes that cover SQL Server, PostgreSQL, MySQL, SQLite, Oracle, and MongoDB scenarios. - Promote the Elsa Deployment Platform PRD into scoped implementation work. [#7469](https://github.com/elsa-workflows/elsa-core/issues/7469) defines the right product boundary: declarative environment manifests, immutable deployment artifacts, dry-run validation, deployment history, and GitOps-compatible reconciliation should manage control-plane state without reconciling runtime execution state. @@ -194,7 +194,7 @@ Recommended success measures: High-value items: -- Finish the diagnostics trilogy: structured logs, console logs, and OpenTelemetry. Structured and console logs now exist; Studio `3.8.0-preview1` ships an OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), and Core still has an active backend PR for OTLP ingestion, bounded stores, REST endpoints, SignalR live updates, collector configuration, and tests in [#7537](https://github.com/elsa-workflows/elsa-core/pull/7537). The remaining product work is to merge and release the Core backend, document collector setup, and correlate this with workflow incidents. +- Finish the diagnostics trilogy: structured logs, console logs, and OpenTelemetry. Structured and console logs now exist; Studio `3.8.0-preview1` ships an OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), and Core `main` now includes the OpenTelemetry backend module in [`src/modules/Elsa.Diagnostics.OpenTelemetry`](src/modules/Elsa.Diagnostics.OpenTelemetry). The remaining product work is to release and operationalize that Core backend, document collector setup, and correlate this with workflow incidents. - Add default workflow semantic metrics: started, resumed, suspended, faulted, completed, active, activity executed/faulted, queue depth, recovery count, drain count, and dispatch latency. [#5988](https://github.com/elsa-workflows/elsa-core/issues/5988) remains the durable demand signal, while [#7537](https://github.com/elsa-workflows/elsa-core/pull/7537) supplies the first current module boundary. - Build Studio diagnostics pages that are useful under pressure: live console, structured logs, OpenTelemetry traces/metrics/logs, workflow incident timelines, source health, dropped-event counters, source selection, filters, URL state, export/copy affordances, and direct deep links to workflow instances. - Make execution history easier to reason about: distinguish faulted, interrupted, cancelled, crash-recovered, retried, and operator-modified workflows consistently across API, Studio, logs, and metrics. @@ -215,7 +215,7 @@ High-value items: - Publish canonical OIDC recipes for Blazor Server, WASM, separate server/studio, all-in-one hosts, and reverse-proxy sub-path deployments. Studio `3.7.0` shipped the modern authentication modules, [#7181](https://github.com/elsa-workflows/elsa-core/issues/7181) shows Core-side implementation and documentation demand, and [elsa-studio#809](https://github.com/elsa-workflows/elsa-studio/pull/809) shows sub-path redirect URI handling is still being hardened. - Provide a production security guide: API keys, JWT/OIDC, default admin bootstrap, scripting trust levels, C# expression risks, Docker demo boundaries, secret masking, tenant isolation, and permission design. - Expand authorization coverage tests around workflow instances, runtime admin, diagnostics, labels, tenants, and HTTP endpoint activities. -- Add Studio governance controls: tenant/role-based activity visibility, permission-aware menus/routes, feature-gated modules, and clear behavior for hidden activities in existing workflow definitions. [elsa-studio#584](https://github.com/elsa-workflows/elsa-studio/issues/584) captures the authoring side of this enterprise need. +- Add Studio governance controls: tenant/role-based activity visibility, granular permission-aware menus/routes, feature-gated modules, and clear behavior for hidden activities in existing workflow definitions. [elsa-studio#584](https://github.com/elsa-workflows/elsa-studio/issues/584) captures the authoring side of this enterprise need, and new issue [elsa-studio#908](https://github.com/elsa-workflows/elsa-studio/issues/908) sharpens the need for the Studio UI to honor granular permissions consistently. - Complete localization and white-label readiness: translation contribution docs, coverage status, missing key checks, branding hooks, and supportable customization patterns. Studio issues and discussions show setup/coverage friction in [elsa-studio#771](https://github.com/elsa-workflows/elsa-studio/issues/771), [elsa-studio discussion #695](https://github.com/elsa-workflows/elsa-studio/discussions/695), and [elsa-studio discussion #678](https://github.com/elsa-workflows/elsa-studio/discussions/678). - Improve multi-tenant ergonomics: tenant-agnostic workflows, high tenant counts, tenant validation modes, cache isolation, and clear migration guidance after the 3.6 tenant ID convention changes. - Create an enterprise deployment checklist for Kubernetes, reverse proxies/base paths, TLS/custom CAs, database migrations, health checks, backups, and disaster recovery. @@ -235,7 +235,7 @@ High-value items: - Build AI-assisted workflow generation that produces multiple visible activities from intent rather than hiding logic in one script activity. This direction is proposed in [discussion #7367](https://github.com/elsa-workflows/elsa-core/discussions/7367), and merged [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) now provides the first Weaver AI Copilot server foundation with AI abstractions, provider/session contracts, chat/tool endpoints, audit events, proposal persistence, EF Core storage, and integration/unit tests. - Provide an Elsa MCP/tooling surface for reading, validating, editing, and explaining workflow JSON/ElsaScript. This would make Elsa a strong fit for AI-enabled .NET development environments. - Align AI authoring with the Extensions Agents work: provider abstractions, MCP tools, OpenAI/Claude/local model support, tool approval, secrets handling, and Studio UX should share contracts instead of creating parallel AI stacks. -- Build a Studio copilot only after the authoring contracts are stable: validation, generated activity metadata, designer APIs, diagnostics links, and test scaffolding should be available before AI generation becomes prominent. [elsa-studio#553](https://github.com/elsa-workflows/elsa-studio/issues/553) has clear community signal and maintainer interest, while merged [#7523](https://github.com/elsa-workflows/elsa-core/pull/7523) is still Core/backend-oriented and should not be treated as a complete Studio product surface. +- Productize the Studio copilot foundation that landed in [elsa-studio#900](https://github.com/elsa-workflows/elsa-studio/pull/900): proposal review/apply flows, validation, generated activity metadata, designer APIs, diagnostics links, and test scaffolding should be available before AI generation becomes prominent. [elsa-studio#553](https://github.com/elsa-workflows/elsa-studio/issues/553) remains the durable demand signal, and the current Studio workspace still depends on Core exposing more proposal/action endpoints. - Add "explain this workflow", "find risky activities", "suggest tests", and "generate migration notes" capabilities backed by workflow graph metadata. - Pair AI generation with validation: generated workflows should include test scaffolds, required input/output definitions, secrets handling, and clear review diffs. @@ -251,7 +251,7 @@ Near term: 1. Finish runtime confidence work: graceful shutdown remaining tasks, recovery clarity, distributed runtime regressions, security documentation, and OIDC recipes. 2. Stabilize Studio authoring: designer regression harness, input/property-editor fixes, async dispatch/run UX, state machine Studio/docs completion, and a clear UI framework direction. -3. Complete the diagnostics trilogy: merge/release the Core OpenTelemetry backend, connect it to the Studio OpenTelemetry page, document collector setup, and correlate traces/logs/metrics with workflow incidents. +3. Complete the diagnostics trilogy: release and operationalize the Core OpenTelemetry backend, connect it to the Studio OpenTelemetry page, document collector setup, and correlate traces/logs/metrics with workflow incidents. 4. Make workflow authoring easier to manage at scale: organization, search, progress/timeline APIs, testing docs, and user preference/table-state persistence. 5. Reconcile shipped extension foundations with roadmap status: package manifests, Connections/Secrets, OpenAPI, Agents, schedulers, messaging, and integration maturity labels. From 33181b2c9dd6417bebc98007084bae1713f51f04 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 11 Jul 2026 14:35:36 +0200 Subject: [PATCH 25/33] test: cover Oracle bulk upsert SQL generation Exercise quoted Oracle identifiers, NVARCHAR2 casts, multi-row SQL, update/insert clauses, and parameter ordering. Reuse the provider SQL generation helper so identifier delimiting stays aligned with Oracle EF Core. --- Elsa.sln | 15 +++++ .../AssemblyInfo.cs | 1 + .../Extensions/BulkUpsertExtensions.cs | 29 ++++----- .../BulkUpsertExtensionsTests.cs | 63 +++++++++++++++++++ .../Elsa.Persistence.EFCore.UnitTests.csproj | 16 +++++ 5 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 test/unit/Elsa.Persistence.EFCore.UnitTests/BulkUpsertExtensionsTests.cs create mode 100644 test/unit/Elsa.Persistence.EFCore.UnitTests/Elsa.Persistence.EFCore.UnitTests.csproj diff --git a/Elsa.sln b/Elsa.sln index 4c0db89a7..60e0b80f5 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -413,6 +413,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.VNext.Runt EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Hosting.Management.UnitTests", "test\unit\Elsa.Hosting.Management.UnitTests\Elsa.Hosting.Management.UnitTests.csproj", "{39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EFCore.UnitTests", "test\unit\Elsa.Persistence.EFCore.UnitTests\Elsa.Persistence.EFCore.UnitTests.csproj", "{C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1837,6 +1839,18 @@ Global {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x64.Build.0 = Release|Any CPU {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x86.ActiveCfg = Release|Any CPU {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6}.Release|x86.Build.0 = Release|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x64.Build.0 = Debug|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Debug|x86.Build.0 = Debug|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|Any CPU.Build.0 = Release|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x64.ActiveCfg = Release|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x64.Build.0 = Release|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x86.ActiveCfg = Release|Any CPU + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1993,6 +2007,7 @@ Global {E1607923-038B-41D6-9D23-F540FC9E6CCE} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {6E3B6948-B16D-480C-879E-B805F732649F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {39DE4EE7-0FDB-499F-9BC2-7ECC779FA5F6} = {18453B51-25EB-4317-A4B3-B10518252E92} + {C6EFA89A-923E-4F7A-A63B-BC8DDF44A208} = {18453B51-25EB-4317-A4B3-B10518252E92} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.Persistence.EFCore.Common/AssemblyInfo.cs b/src/modules/Elsa.Persistence.EFCore.Common/AssemblyInfo.cs index 77206ee4c..eb90fe57f 100644 --- a/src/modules/Elsa.Persistence.EFCore.Common/AssemblyInfo.cs +++ b/src/modules/Elsa.Persistence.EFCore.Common/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Elsa.Persistence.EFCore")] +[assembly: InternalsVisibleTo("Elsa.Persistence.EFCore.UnitTests")] diff --git a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs index 707702589..1d20f979c 100644 --- a/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs +++ b/src/modules/Elsa.Persistence.EFCore.Common/Extensions/BulkUpsertExtensions.cs @@ -1,8 +1,9 @@ +using System.Linq.Expressions; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Storage; // ReSharper disable once CheckNamespace namespace Elsa.Persistence.EFCore.Extensions; @@ -337,7 +338,7 @@ public static class BulkUpsertExtensions return (sb.ToString(), parameters.ToArray()); } - private static (string, object[]) GenerateOracleUpsert( + internal static (string, object[]) GenerateOracleUpsert( DbContext dbContext, IList entities, Expression> keySelector) @@ -347,12 +348,8 @@ public static class BulkUpsertExtensions var schema = entityType.GetSchema(); var tableName = entityType.GetTableName()!; var storeObject = StoreObjectIdentifier.Table(tableName, schema); - - // Both schema and table must be quoted so Oracle treats them as case-sensitive - // identifiers, matching what EF Core migrations create. - var fullName = !string.IsNullOrEmpty(schema) - ? $"\"{schema}\".\"{tableName}\"" - : $"\"{tableName}\""; + var sqlGenerationHelper = dbContext.GetService(); + var fullName = sqlGenerationHelper.DelimitIdentifier(tableName, schema); var props = entityType.GetProperties().ToList(); @@ -361,9 +358,9 @@ public static class BulkUpsertExtensions // Pre-build quoted column names once and reuse throughout all clauses. var quotedColumnNames = props - .Select(p => $"\"{p.GetColumnName(storeObject)}\"") + .Select(p => sqlGenerationHelper.DelimitIdentifier(p.GetColumnName(storeObject)!)) .ToList(); - var quotedKeyColumnName = $"\"{keyColumnName}\""; + var quotedKeyColumnName = sqlGenerationHelper.DelimitIdentifier(keyColumnName); var sb = new StringBuilder(); var parameters = new List(); @@ -390,23 +387,23 @@ public static class BulkUpsertExtensions value = converter.ConvertToProvider(value); parameters.Add(value!); - + // Aliases must be quoted so Oracle preserves their case, matching // the quoted references in ON, UPDATE SET, and INSERT/VALUES below. - var quotedAlias = $"\"{property.GetColumnName(storeObject)}\""; - + var quotedAlias = sqlGenerationHelper.DelimitIdentifier(property.GetColumnName(storeObject)!); + // In a SELECT … FROM DUAL subquery, ODP.NET has no target column to // derive bind parameter types from and defaults to VARCHAR2 for .NET // strings. Elsa's Oracle migrations define string columns as NVARCHAR2, // so an explicit CAST is required to avoid a datatype mismatch error. - // The full EF Core column type string (e.g. "NVARCHAR2(450 CHAR)") is + // The full EF Core column type string (e.g. "NVARCHAR2(450)") is // used directly in the CAST so all Oracle type variants are handled // correctly without any string parsing. var columnType = property.GetColumnType() ?? string.Empty; var expr = columnType.StartsWith("NVARCHAR2", StringComparison.OrdinalIgnoreCase) ? $"CAST({paramName} AS {columnType})" : paramName; - + lineParts.Add($"{expr} AS {quotedAlias}"); } @@ -425,4 +422,4 @@ public static class BulkUpsertExtensions return (sb.ToString(), parameters.ToArray()); } -} \ No newline at end of file +} diff --git a/test/unit/Elsa.Persistence.EFCore.UnitTests/BulkUpsertExtensionsTests.cs b/test/unit/Elsa.Persistence.EFCore.UnitTests/BulkUpsertExtensionsTests.cs new file mode 100644 index 000000000..d0d1e24b5 --- /dev/null +++ b/test/unit/Elsa.Persistence.EFCore.UnitTests/BulkUpsertExtensionsTests.cs @@ -0,0 +1,63 @@ +using Elsa.Persistence.EFCore.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Persistence.EFCore.UnitTests; + +public class BulkUpsertExtensionsTests +{ + [Fact] + public void GenerateOracleUpsert_ProducesQuotedMergeWithNvarcharCasts() + { + using var dbContext = CreateDbContext(); + var entities = new List + { + new() { Id = "first", Name = "First", Count = 1 }, + new() { Id = "second", Name = null, Count = 2 } + }; + + var (sql, parameters) = BulkUpsertExtensions.GenerateOracleUpsert(dbContext, entities, x => x.Id); + + Assert.Contains("MERGE INTO \"Elsa\".\"ActivityExecutionRecords\" Target", sql); + Assert.Contains("CAST({0} AS NVARCHAR2(450)) AS \"RecordId\"", sql); + Assert.Contains("CAST({2} AS NVARCHAR2(2000)) AS \"DisplayName\"", sql); + Assert.Contains("CAST({5} AS NVARCHAR2(2000)) AS \"DisplayName\"", sql); + Assert.Contains("FROM DUAL UNION ALL SELECT", sql); + Assert.Contains("Target.\"RecordId\" = Source.\"RecordId\"", sql); + Assert.Contains("INSERT (\"RecordId\", \"Count\", \"DisplayName\")", sql); + Assert.Contains("VALUES (Source.\"RecordId\", Source.\"Count\", Source.\"DisplayName\")", sql); + Assert.DoesNotContain("CAST({1} AS NUMBER", sql); + + var updateClause = sql[sql.IndexOf("WHEN MATCHED", StringComparison.Ordinal)..sql.IndexOf("WHEN NOT MATCHED", StringComparison.Ordinal)]; + Assert.DoesNotContain("Target.\"RecordId\" = Source.\"RecordId\"", updateClause); + Assert.Contains("Target.\"DisplayName\" = Source.\"DisplayName\"", updateClause); + Assert.Equal(new object?[] { "first", 1, "First", "second", 2, null }, parameters); + } + + private static TestDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseOracle("Data Source=unused") + .Options; + return new TestDbContext(options); + } + + private sealed class TestDbContext(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.ToTable("ActivityExecutionRecords", "Elsa"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Id).HasColumnName("RecordId").HasColumnType("NVARCHAR2(450)"); + entity.Property(x => x.Count).HasColumnType("NUMBER(10)"); + entity.Property(x => x.Name).HasColumnName("DisplayName").HasColumnType("NVARCHAR2(2000)"); + } + } + + private sealed class TestEntity + { + public string Id { get; set; } = null!; + public int Count { get; set; } + public string? Name { get; set; } + } +} diff --git a/test/unit/Elsa.Persistence.EFCore.UnitTests/Elsa.Persistence.EFCore.UnitTests.csproj b/test/unit/Elsa.Persistence.EFCore.UnitTests/Elsa.Persistence.EFCore.UnitTests.csproj new file mode 100644 index 000000000..50b51c57e --- /dev/null +++ b/test/unit/Elsa.Persistence.EFCore.UnitTests/Elsa.Persistence.EFCore.UnitTests.csproj @@ -0,0 +1,16 @@ + + + + [Elsa.Persistence.EFCore]* + 0 + + + + + + + + + + + From a73ccfd2a3919984c4d366f0706b5f3660d565c0 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 12 Jul 2026 10:33:52 +0200 Subject: [PATCH 26/33] chore: update patch dependencies (#7766) * chore: update patch dependencies * chore: align remaining net10 packages --- Directory.Packages.props | 130 +++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a01eb1908..a21b268c1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,39 +8,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - + + + @@ -50,39 +50,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - + + + @@ -183,11 +183,11 @@ - + - + - + From 3736eca19b3220e4b5d76f8596c27dbf4b3a59a6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 14 Jul 2026 23:03:03 +0200 Subject: [PATCH 27/33] chore: make agent instructions feature-neutral --- .agents/skills/speckit-plan/SKILL.md | 4 +- .claude/skills/speckit-plan/SKILL.md | 8 +- .github/agents/copilot-instructions.md | 30 +- .github/agents/speckit.plan.agent.md | 8 +- .github/copilot-instructions.md | 265 +----- .specify/scripts/bash/update-agent-context.sh | 830 +----------------- .specify/templates/agent-file-template.md | 26 +- AGENTS.md | 27 +- CLAUDE.md | 37 +- 9 files changed, 22 insertions(+), 1213 deletions(-) diff --git a/.agents/skills/speckit-plan/SKILL.md b/.agents/skills/speckit-plan/SKILL.md index c4be84923..b761b014b 100644 --- a/.agents/skills/speckit-plan/SKILL.md +++ b/.agents/skills/speckit-plan/SKILL.md @@ -137,9 +137,9 @@ You **MUST** consider the user input before proceeding (if not empty). - Skip if project is purely internal (build scripts, one-off tools, etc.) 3. **Agent context update**: - - Update the plan reference between the `` and `` markers in `AGENTS.md` to point to the plan file created in step 1 (the IMPL_PLAN path) + - Do not copy plan-derived context into `AGENTS.md` or agent-specific instruction files. Repository instructions are intentionally feature-neutral; feature context remains in the plan artifacts. -**Output**: data-model.md, /contracts/*, quickstart.md, updated agent context file +**Output**: data-model.md, /contracts/*, quickstart.md ## Key rules diff --git a/.claude/skills/speckit-plan/SKILL.md b/.claude/skills/speckit-plan/SKILL.md index a2f723c06..e272afe71 100644 --- a/.claude/skills/speckit-plan/SKILL.md +++ b/.claude/skills/speckit-plan/SKILL.md @@ -138,13 +138,9 @@ You **MUST** consider the user input before proceeding (if not empty). - Skip if project is purely internal (build scripts, one-off tools, etc.) 3. **Agent context update**: - - Run `.specify/scripts/bash/update-agent-context.sh claude` - - These scripts detect which AI agent is in use - - Update the appropriate agent-specific context file - - Add only new technology from current plan - - Preserve manual additions between markers + - Do not run the legacy plan-to-agent-context updater. Repository instructions are intentionally feature-neutral; keep feature context in the plan artifacts. -**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file +**Output**: data-model.md, /contracts/*, quickstart.md ## Key rules diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md index e410cb1f5..b2a55b112 100644 --- a/.github/agents/copilot-instructions.md +++ b/.github/agents/copilot-instructions.md @@ -1,29 +1,3 @@ -# main Development Guidelines +# Elsa Core Agent Instructions -Auto-generated from all feature plans. Last updated: 2026-03-08 - -## Active Technologies - -- C# latest on .NET 10.0 primary, with existing multi-target support for .NET 8.0 and .NET 9.0 + FastEndpoints, Elsa.Api.Common abstractions, CShells, CShells.FastEndpoints.Abstractions, Refit client contracts, xUnit component test infrastructure (001-shell-reload-api) - -## Project Structure - -```text -src/ -tests/ -``` - -## Commands - -# Add commands for C# latest on .NET 10.0 primary, with existing multi-target support for .NET 8.0 and .NET 9.0 - -## Code Style - -C# latest on .NET 10.0 primary, with existing multi-target support for .NET 8.0 and .NET 9.0: Follow standard conventions - -## Recent Changes - -- 001-shell-reload-api: Added C# latest on .NET 10.0 primary, with existing multi-target support for .NET 8.0 and .NET 9.0 + FastEndpoints, Elsa.Api.Common abstractions, CShells, CShells.FastEndpoints.Abstractions, Refit client contracts, xUnit component test infrastructure - - - +Use the canonical repository guidance in [AGENTS.md](../../AGENTS.md). Keep feature-specific plan details in `specs/`; they are not copied into this file. diff --git a/.github/agents/speckit.plan.agent.md b/.github/agents/speckit.plan.agent.md index 0ffb929d0..48ac6695f 100644 --- a/.github/agents/speckit.plan.agent.md +++ b/.github/agents/speckit.plan.agent.md @@ -76,13 +76,9 @@ You **MUST** consider the user input before proceeding (if not empty). - Skip if project is purely internal (build scripts, one-off tools, etc.) 3. **Agent context update**: - - Run `.specify/scripts/bash/update-agent-context.sh copilot` - - These scripts detect which AI agent is in use - - Update the appropriate agent-specific context file - - Add only new technology from current plan - - Preserve manual additions between markers + - Do not copy plan-derived context into `AGENTS.md` or agent-specific instruction files. The compatibility updater is intentionally disabled so repository instructions remain feature-neutral. -**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file +**Output**: data-model.md, /contracts/*, quickstart.md ## Key rules diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2cb307856..6003fd0f1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,264 +1,3 @@ -# Copilot Coding Agent Instructions for Elsa Workflows +# Elsa Core Coding Agent Instructions -## Repository Overview - -**Elsa Workflows** is a powerful .NET workflow library that enables workflow execution within any .NET application. This is version 3.0, supporting .NET 8.0, .NET 9.0 and .NET 10.0 and providing both a visual designer (from a different repository, elsa-studio) and programmatic workflow definition capabilities. - -### Key Statistics -- **Language**: C# (.NET 10.0) -- **Architecture**: Modular library with 104+ projects -- **Code Size**: ~3,500 C# files across modules -- **License**: MIT -- **Build System**: NUKE build automation -- **Target Frameworks**: .NET 10.0 (primary) - -## High-Level Architecture - -### Directory Structure -``` -src/ -├── apps/ # Reference applications (5 projects) -│ ├── Elsa.Server.Web # Workflow server only -│ └── Elsa.Server.LoadBalancer # Load balancer -├── common/ # Shared libraries (8 projects) -├── modules/ # Core functionality modules (70+ projects) -│ ├── Elsa.Workflows.Core # Core workflow engine -│ ├── Elsa.Workflows.Runtime # Runtime execution -│ ├── Elsa.Workflows.Api # REST API -│ ├── Elsa.Http # HTTP activities -│ ├── Elsa.Email # Email activities -│ └── [many others] # Database, messaging, etc. -└── clients/ # API clients -test/ -├── unit/ # Unit tests -├── integration/ # Integration tests -├── component/ # Component tests -└── performance/ # Performance tests -build/ # NUKE build configuration -docker/ # Docker configurations -``` - -### Core Components -- **Elsa.Workflows.Core**: Main workflow engine and activities -- **Elsa.Workflows.Runtime**: Workflow execution runtime -- **Elsa.Workflows.Api**: RESTful API for workflow management -- **Elsa.Workflows.Management**: Workflow definition management -- **Elsa modules**: Specialized functionality (HTTP, persistence, scheduling, etc.) - -## Build Instructions - -### Prerequisites -- **.NET 10.0 SDK** -- **Build time**: Initial restore ~1-2 minutes, full compile ~5-10 minutes - -### Build Commands - -**Primary build script**: `./build.sh` (Linux/macOS) or `.\build.cmd` (Windows) - -```bash -# View available targets -./build.sh --help - -# Clean build artifacts -./build.sh Clean - -# Restore packages (may show warnings for inaccessible feeds) -./build.sh Restore --ignore-failed-sources - -# Compile core components (excludes studio apps) -./build.sh Compile - -# Run tests (limited due to external dependencies) -./build.sh Test - -# Create NuGet packages -./build.sh Pack - -# Full CI pipeline (compile, test, pack) -./build.sh Compile Test Pack -``` - -**Direct dotnet commands** for core components: -```bash -# Build specific core projects that don't require external packages -dotnet restore src/modules/Elsa.Workflows.Core/ --ignore-failed-sources -dotnet build src/modules/Elsa.Workflows.Core/ --no-restore -dotnet restore src/modules/Elsa.Workflows.Runtime/ --ignore-failed-sources -dotnet build src/modules/Elsa.Workflows.Runtime/ --no-restore - -# Note: Server apps may fail due to WebhooksCore dependency -# Build and test individual core modules -find test/unit -name "*.csproj" | head -5 | xargs -I {} dotnet build {} -``` - -### Expected Build Warnings -- `NU1900`: Unable to load service index for external feeds (safe to ignore) -- `NU1801`: Service index warnings for feedz.io sources (safe to ignore) - -### Successful Build Indicators -- Core modules (Elsa.Workflows.Core, etc.) compile successfully -- Server applications (Elsa.Server.Web) build -- Most modules show "succeeded with X warning(s)" (warnings are acceptable) - -## Testing - -### Test Structure -- **Unit tests**: `test/unit/` - Fast, isolated tests -- **Integration tests**: `test/integration/` - End-to-end scenarios -- **Component tests**: `test/component/` - Feature testing -- **Performance tests**: `test/performance/` - Benchmarks - -### Running Tests -```bash -# Via NUKE build system -./build.sh Test - -# Direct dotnet test (for accessible projects) -dotnet test test/unit/[specific-project]/ -dotnet test --no-build --no-restore [project-path] -``` - -**Note**: Many tests may fail to run due to external package dependencies. Focus on core workflow engine tests that don't require studio packages. - -## Development Guidelines - -### Code Standards -- **Language version**: C# latest -- **Target framework**: .NET 10.0 -- **Nullable reference types**: Enabled -- **Implicit usings**: Enabled -- **EditorConfig**: Configured (4-space indentation, CRLF line endings) - -### Architecture Patterns -- **Modular design**: Each feature area is a separate project/module -- **Dependency injection**: Heavy use of Microsoft.Extensions.DependencyInjection -- **Activity-based**: Workflows are built from composable activities -- **Async/await**: Extensive use throughout for scalability - -### Common Gotchas -1. **NuGet Source Mapping**: Configured in NuGet.Config, restricts where packages can be sourced -2**Multiple Target Frameworks**: Some projects conditionally target different frameworks -3**Build Warnings**: Many NU1900/NU1801 warnings are expected and safe - -## Continuous Integration - -### GitHub Actions Workflow -- **Trigger**: Pull requests to `main` branch -- **Runner**: ubuntu-latest -- **.NET Version**: 10.x (latest) -- **Commands**: `./build.cmd Compile Test Pack` -- **File**: `.github/workflows/pr.yml` (auto-generated by NUKE) - -### CI Pipeline Steps -1. Checkout code -2. Setup .NET 10.x SDK -3. Execute: Compile → Test → Pack -4. Expected warnings for external feed access -5. Studio apps may be excluded from CI builds - -## Key Configuration Files - -- **Build**: `build/Build.cs` (NUKE build configuration) -- **Dependencies**: `Directory.Packages.props` (central package management) -- **Global settings**: `Directory.Build.props` -- **NuGet**: `NuGet.Config` (package sources and mapping) -- **Solution**: `Elsa.sln` (119 projects) -- **Docker**: `docker/` directory with multiple Dockerfiles -- **GitHub Actions**: `.github/workflows/` (auto-generated) - -## Docker Support - -Multiple Docker configurations available: -- `ElsaServer.Dockerfile` - Server only -- `ElsaServerAndStudio.Dockerfile` - Combined server + studio -- `ElsaStudio.Dockerfile` - Studio only -- Docker Compose configurations for development - -## Quick Start for Development - -1. **Clone and build core components**: - ```bash - git clone [repo-url] - cd elsa-core - ./build.sh Clean Restore --ignore-failed-sources - ``` - -2. **Work with core modules** (avoid studio dependencies): - ```bash - cd src/modules/Elsa.Workflows.Core - dotnet build - dotnet test ../../test/unit/[related-tests]/ - ``` - -3. **Work with core modules that don't require external dependencies**: - ```bash - cd src/modules/Elsa.Workflows.Core - dotnet restore --ignore-failed-sources - dotnet build --no-restore - ``` - -## Running the Applications - -### Development Workflow Server - -To run the workflow server for development: - -```bash -cd src/apps/Elsa.Server.Web -dotnet restore --ignore-failed-sources -dotnet run -``` - -The server will start on the configured ports (check `appsettings.json` or environment variables). - -## Troubleshooting - -### Common Issues - -1. **Missing External Packages**: If you encounter `NU1101` errors for Elsa.Studio packages: - - This is expected for studio-related apps - - Focus development on core workflow modules instead - - Or use Docker images that have pre-built studio components - -2. **Build Fails on Server Apps**: If `Elsa.Server.Web` fails due to WebhooksCore: - - This is a known issue with external package feeds - - Try building individual core modules instead - - Use `--ignore-failed-sources` flag consistently - -3. **Test Failures**: If many tests fail to run: - - External package dependencies may be unavailable - - Run tests for specific core modules individually - - Focus on tests that don't require studio packages - -4. **Slow Initial Build**: First restore and compile can take 5-10+ minutes: - - This is normal for a large solution with 100+ projects - - Subsequent builds are much faster (incremental) - - Consider building specific projects/modules when iterating - -## Additional Resources - -### Documentation -- **Official Documentation**: [https://docs.elsaworkflows.io/](https://docs.elsaworkflows.io/) -- **README**: See [README.md](../README.md) for quick start and features overview -- **Contributing Guide**: See [CONTRIBUTING.md](../CONTRIBUTING.md) for contribution guidelines - -### Community Support -- **GitHub Issues**: [Report bugs and request features](https://github.com/elsa-workflows/elsa-core/issues) -- **GitHub Discussions**: [Ask questions and discuss](https://github.com/elsa-workflows/elsa-core/discussions) -- **Discord**: [Join the community chat](https://discord.gg/hhChk5H472) -- **Stack Overflow**: [Tag: elsa-workflows](http://stackoverflow.com/questions/tagged/elsa-workflows) - -### Enterprise Support -- **ELSA-X**: [Professional support and enterprise solutions](https://elsa-x.io) - -## Important Notes for Coding Agents - -1. **Always use `--ignore-failed-sources`** when restoring packages -2. **Focus on core workflow functionality** -3. **Build warnings are normal** - don't try to fix NU1900/NU1801 warnings -4. **Test individual modules** rather than solution-wide tests when external deps fail -5. **Use direct dotnet commands** for building specific components when NUKE fails -6. **Check project references** before attempting builds - some projects have conditional references -7. **Start with core modules** like `Elsa.Workflows.Core`, `Elsa.Workflows.Runtime` which are more likely to build successfully - -Trust these instructions for build and development workflows. Only search for additional information if these instructions are incomplete or found to be incorrect. \ No newline at end of file +Use the canonical repository guidance in [AGENTS.md](../AGENTS.md). It defines the supported layout, commands, conventions, and testing guidance. Keep feature-specific plan details in `specs/`; they are not copied into this file. diff --git a/.specify/scripts/bash/update-agent-context.sh b/.specify/scripts/bash/update-agent-context.sh index fdebac65f..5fdea8f5f 100755 --- a/.specify/scripts/bash/update-agent-context.sh +++ b/.specify/scripts/bash/update-agent-context.sh @@ -1,829 +1,9 @@ #!/usr/bin/env bash -# Update agent context files with information from plan.md -# -# This script maintains AI agent context files by parsing feature specifications -# and updating agent-specific configuration files with project information. -# -# MAIN FUNCTIONS: -# 1. Environment Validation -# - Verifies git repository structure and branch information -# - Checks for required plan.md files and templates -# - Validates file permissions and accessibility -# -# 2. Plan Data Extraction -# - Parses plan.md files to extract project metadata -# - Identifies language/version, frameworks, databases, and project types -# - Handles missing or incomplete specification data gracefully -# -# 3. Agent File Management -# - Creates new agent context files from templates when needed -# - Updates existing agent files with new project information -# - Preserves manual additions and custom configurations -# - Supports multiple AI agent formats and directory structures -# -# 4. Content Generation -# - Generates language-specific build/test commands -# - Creates appropriate project directory structures -# - Updates technology stacks and recent changes sections -# - Maintains consistent formatting and timestamps -# -# 5. Multi-Agent Support -# - Handles agent-specific file paths and naming conventions -# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity -# - Can update single agents or all existing agent files -# - Creates default Claude file if no agent files exist -# -# Usage: ./update-agent-context.sh [agent_type] -# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli -# Leave empty to update all existing agent files +# Retained as a Speckit compatibility entry point. Plan-derived agent context +# is intentionally disabled so feature plans cannot rewrite repository-wide +# instructions with stale technology or historical-change summaries. -set -e +set -euo pipefail -# Enable strict error handling -set -u -set -o pipefail - -#============================================================================== -# Configuration and Global Variables -#============================================================================== - -# Get script directory and load common functions -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths and variables from common functions -eval $(get_feature_paths) - -NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code -AGENT_TYPE="${1:-}" - -# Agent-specific file paths -CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" -GEMINI_FILE="$REPO_ROOT/GEMINI.md" -COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md" -CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" -QWEN_FILE="$REPO_ROOT/QWEN.md" -AGENTS_FILE="$REPO_ROOT/AGENTS.md" -WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" -KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" -AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" -ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" -CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" -QODER_FILE="$REPO_ROOT/QODER.md" -AMP_FILE="$REPO_ROOT/AGENTS.md" -SHAI_FILE="$REPO_ROOT/SHAI.md" -KIRO_FILE="$REPO_ROOT/AGENTS.md" -AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md" -BOB_FILE="$REPO_ROOT/AGENTS.md" - -# Template file -TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" - -# Global variables for parsed plan data -NEW_LANG="" -NEW_FRAMEWORK="" -NEW_DB="" -NEW_PROJECT_TYPE="" - -#============================================================================== -# Utility Functions -#============================================================================== - -log_info() { - echo "INFO: $1" -} - -log_success() { - echo "✓ $1" -} - -log_error() { - echo "ERROR: $1" >&2 -} - -log_warning() { - echo "WARNING: $1" >&2 -} - -# Cleanup function for temporary files -cleanup() { - local exit_code=$? - rm -f /tmp/agent_update_*_$$ - rm -f /tmp/manual_additions_$$ - exit $exit_code -} - -# Set up cleanup trap -trap cleanup EXIT INT TERM - -#============================================================================== -# Validation Functions -#============================================================================== - -validate_environment() { - # Check if we have a current branch/feature (git or non-git) - if [[ -z "$CURRENT_BRANCH" ]]; then - log_error "Unable to determine current feature" - if [[ "$HAS_GIT" == "true" ]]; then - log_info "Make sure you're on a feature branch" - else - log_info "Set SPECIFY_FEATURE environment variable or create a feature first" - fi - exit 1 - fi - - # Check if plan.md exists - if [[ ! -f "$NEW_PLAN" ]]; then - log_error "No plan.md found at $NEW_PLAN" - log_info "Make sure you're working on a feature with a corresponding spec directory" - if [[ "$HAS_GIT" != "true" ]]; then - log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first" - fi - exit 1 - fi - - # Check if template exists (needed for new files) - if [[ ! -f "$TEMPLATE_FILE" ]]; then - log_warning "Template file not found at $TEMPLATE_FILE" - log_warning "Creating new agent files will fail" - fi -} - -#============================================================================== -# Plan Parsing Functions -#============================================================================== - -extract_plan_field() { - local field_pattern="$1" - local plan_file="$2" - - grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ - head -1 | \ - sed "s|^\*\*${field_pattern}\*\*: ||" | \ - sed 's/^[ \t]*//;s/[ \t]*$//' | \ - grep -v "NEEDS CLARIFICATION" | \ - grep -v "^N/A$" || echo "" -} - -parse_plan_data() { - local plan_file="$1" - - if [[ ! -f "$plan_file" ]]; then - log_error "Plan file not found: $plan_file" - return 1 - fi - - if [[ ! -r "$plan_file" ]]; then - log_error "Plan file is not readable: $plan_file" - return 1 - fi - - log_info "Parsing plan data from $plan_file" - - NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") - NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") - NEW_DB=$(extract_plan_field "Storage" "$plan_file") - NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") - - # Log what we found - if [[ -n "$NEW_LANG" ]]; then - log_info "Found language: $NEW_LANG" - else - log_warning "No language information found in plan" - fi - - if [[ -n "$NEW_FRAMEWORK" ]]; then - log_info "Found framework: $NEW_FRAMEWORK" - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then - log_info "Found database: $NEW_DB" - fi - - if [[ -n "$NEW_PROJECT_TYPE" ]]; then - log_info "Found project type: $NEW_PROJECT_TYPE" - fi -} - -format_technology_stack() { - local lang="$1" - local framework="$2" - local parts=() - - # Add non-empty parts - [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") - [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") - - # Join with proper formatting - if [[ ${#parts[@]} -eq 0 ]]; then - echo "" - elif [[ ${#parts[@]} -eq 1 ]]; then - echo "${parts[0]}" - else - # Join multiple parts with " + " - local result="${parts[0]}" - for ((i=1; i<${#parts[@]}; i++)); do - result="$result + ${parts[i]}" - done - echo "$result" - fi -} - -#============================================================================== -# Template and Content Generation Functions -#============================================================================== - -get_project_structure() { - local project_type="$1" - - if [[ "$project_type" == *"web"* ]]; then - echo "backend/\\nfrontend/\\ntests/" - else - echo "src/\\ntests/" - fi -} - -get_commands_for_language() { - local lang="$1" - - case "$lang" in - *"Python"*) - echo "cd src && pytest && ruff check ." - ;; - *"Rust"*) - echo "cargo test && cargo clippy" - ;; - *"JavaScript"*|*"TypeScript"*) - echo "npm test \\&\\& npm run lint" - ;; - *) - echo "# Add commands for $lang" - ;; - esac -} - -get_language_conventions() { - local lang="$1" - echo "$lang: Follow standard conventions" -} - -create_new_agent_file() { - local target_file="$1" - local temp_file="$2" - local project_name="$3" - local current_date="$4" - - if [[ ! -f "$TEMPLATE_FILE" ]]; then - log_error "Template not found at $TEMPLATE_FILE" - return 1 - fi - - if [[ ! -r "$TEMPLATE_FILE" ]]; then - log_error "Template file is not readable: $TEMPLATE_FILE" - return 1 - fi - - log_info "Creating new agent context file from template..." - - if ! cp "$TEMPLATE_FILE" "$temp_file"; then - log_error "Failed to copy template file" - return 1 - fi - - # Replace template placeholders - local project_structure - project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") - - local commands - commands=$(get_commands_for_language "$NEW_LANG") - - local language_conventions - language_conventions=$(get_language_conventions "$NEW_LANG") - - # Perform substitutions with error checking using safer approach - # Escape special characters for sed by using a different delimiter or escaping - local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') - local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') - local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') - - # Build technology stack and recent change strings conditionally - local tech_stack - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then - tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)" - elif [[ -n "$escaped_lang" ]]; then - tech_stack="- $escaped_lang ($escaped_branch)" - elif [[ -n "$escaped_framework" ]]; then - tech_stack="- $escaped_framework ($escaped_branch)" - else - tech_stack="- ($escaped_branch)" - fi - - local recent_change - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then - recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework" - elif [[ -n "$escaped_lang" ]]; then - recent_change="- $escaped_branch: Added $escaped_lang" - elif [[ -n "$escaped_framework" ]]; then - recent_change="- $escaped_branch: Added $escaped_framework" - else - recent_change="- $escaped_branch: Added" - fi - - local substitutions=( - "s|\[PROJECT NAME\]|$project_name|" - "s|\[DATE\]|$current_date|" - "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|" - "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g" - "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|" - "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" - "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" - ) - - for substitution in "${substitutions[@]}"; do - if ! sed -i.bak -e "$substitution" "$temp_file"; then - log_error "Failed to perform substitution: $substitution" - rm -f "$temp_file" "$temp_file.bak" - return 1 - fi - done - - # Convert \n sequences to actual newlines - newline=$(printf '\n') - sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" - - # Clean up backup files - rm -f "$temp_file.bak" "$temp_file.bak2" - - # Prepend Cursor frontmatter for .mdc files so rules are auto-included - if [[ "$target_file" == *.mdc ]]; then - local frontmatter_file - frontmatter_file=$(mktemp) || return 1 - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" - cat "$temp_file" >> "$frontmatter_file" - mv "$frontmatter_file" "$temp_file" - fi - - return 0 -} - - - - -update_existing_agent_file() { - local target_file="$1" - local current_date="$2" - - log_info "Updating existing agent context file..." - - # Use a single temporary file for atomic update - local temp_file - temp_file=$(mktemp) || { - log_error "Failed to create temporary file" - return 1 - } - - # Process the file in one pass - local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") - local new_tech_entries=() - local new_change_entry="" - - # Prepare new technology entries - if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then - new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then - new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") - fi - - # Prepare new change entry - if [[ -n "$tech_stack" ]]; then - new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" - elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then - new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" - fi - - # Check if sections exist in the file - local has_active_technologies=0 - local has_recent_changes=0 - - if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then - has_active_technologies=1 - fi - - if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then - has_recent_changes=1 - fi - - # Process file line by line - local in_tech_section=false - local in_changes_section=false - local tech_entries_added=false - local changes_entries_added=false - local existing_changes_count=0 - local file_ended=false - - while IFS= read -r line || [[ -n "$line" ]]; do - # Handle Active Technologies section - if [[ "$line" == "## Active Technologies" ]]; then - echo "$line" >> "$temp_file" - in_tech_section=true - continue - elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then - # Add new tech entries before closing the section - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - echo "$line" >> "$temp_file" - in_tech_section=false - continue - elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then - # Add new tech entries before empty line in tech section - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - echo "$line" >> "$temp_file" - continue - fi - - # Handle Recent Changes section - if [[ "$line" == "## Recent Changes" ]]; then - echo "$line" >> "$temp_file" - # Add new change entry right after the heading - if [[ -n "$new_change_entry" ]]; then - echo "$new_change_entry" >> "$temp_file" - fi - in_changes_section=true - changes_entries_added=true - continue - elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then - echo "$line" >> "$temp_file" - in_changes_section=false - continue - elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then - # Keep only first 2 existing changes - if [[ $existing_changes_count -lt 2 ]]; then - echo "$line" >> "$temp_file" - ((existing_changes_count++)) - fi - continue - fi - - # Update timestamp - if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then - echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" - else - echo "$line" >> "$temp_file" - fi - done < "$target_file" - - # Post-loop check: if we're still in the Active Technologies section and haven't added new entries - if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - - # If sections don't exist, add them at the end of the file - if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - echo "" >> "$temp_file" - echo "## Active Technologies" >> "$temp_file" - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - - if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then - echo "" >> "$temp_file" - echo "## Recent Changes" >> "$temp_file" - echo "$new_change_entry" >> "$temp_file" - changes_entries_added=true - fi - - # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion - if [[ "$target_file" == *.mdc ]]; then - if ! head -1 "$temp_file" | grep -q '^---'; then - local frontmatter_file - frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; } - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" - cat "$temp_file" >> "$frontmatter_file" - mv "$frontmatter_file" "$temp_file" - fi - fi - - # Move temp file to target atomically - if ! mv "$temp_file" "$target_file"; then - log_error "Failed to update target file" - rm -f "$temp_file" - return 1 - fi - - return 0 -} -#============================================================================== -# Main Agent File Update Function -#============================================================================== - -update_agent_file() { - local target_file="$1" - local agent_name="$2" - - if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then - log_error "update_agent_file requires target_file and agent_name parameters" - return 1 - fi - - log_info "Updating $agent_name context file: $target_file" - - local project_name - project_name=$(basename "$REPO_ROOT") - local current_date - current_date=$(date +%Y-%m-%d) - - # Create directory if it doesn't exist - local target_dir - target_dir=$(dirname "$target_file") - if [[ ! -d "$target_dir" ]]; then - if ! mkdir -p "$target_dir"; then - log_error "Failed to create directory: $target_dir" - return 1 - fi - fi - - if [[ ! -f "$target_file" ]]; then - # Create new file from template - local temp_file - temp_file=$(mktemp) || { - log_error "Failed to create temporary file" - return 1 - } - - if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then - if mv "$temp_file" "$target_file"; then - log_success "Created new $agent_name context file" - else - log_error "Failed to move temporary file to $target_file" - rm -f "$temp_file" - return 1 - fi - else - log_error "Failed to create new agent file" - rm -f "$temp_file" - return 1 - fi - else - # Update existing file - if [[ ! -r "$target_file" ]]; then - log_error "Cannot read existing file: $target_file" - return 1 - fi - - if [[ ! -w "$target_file" ]]; then - log_error "Cannot write to existing file: $target_file" - return 1 - fi - - if update_existing_agent_file "$target_file" "$current_date"; then - log_success "Updated existing $agent_name context file" - else - log_error "Failed to update existing agent file" - return 1 - fi - fi - - return 0 -} - -#============================================================================== -# Agent Selection and Processing -#============================================================================== - -update_specific_agent() { - local agent_type="$1" - - case "$agent_type" in - claude) - update_agent_file "$CLAUDE_FILE" "Claude Code" - ;; - gemini) - update_agent_file "$GEMINI_FILE" "Gemini CLI" - ;; - copilot) - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - ;; - cursor-agent) - update_agent_file "$CURSOR_FILE" "Cursor IDE" - ;; - qwen) - update_agent_file "$QWEN_FILE" "Qwen Code" - ;; - opencode) - update_agent_file "$AGENTS_FILE" "opencode" - ;; - codex) - update_agent_file "$AGENTS_FILE" "Codex CLI" - ;; - windsurf) - update_agent_file "$WINDSURF_FILE" "Windsurf" - ;; - kilocode) - update_agent_file "$KILOCODE_FILE" "Kilo Code" - ;; - auggie) - update_agent_file "$AUGGIE_FILE" "Auggie CLI" - ;; - roo) - update_agent_file "$ROO_FILE" "Roo Code" - ;; - codebuddy) - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" - ;; - qodercli) - update_agent_file "$QODER_FILE" "Qoder CLI" - ;; - amp) - update_agent_file "$AMP_FILE" "Amp" - ;; - shai) - update_agent_file "$SHAI_FILE" "SHAI" - ;; - kiro-cli) - update_agent_file "$KIRO_FILE" "Kiro CLI" - ;; - agy) - update_agent_file "$AGY_FILE" "Antigravity" - ;; - bob) - update_agent_file "$BOB_FILE" "IBM Bob" - ;; - generic) - log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent." - ;; - *) - log_error "Unknown agent type '$agent_type'" - log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic" - exit 1 - ;; - esac -} - -update_all_existing_agents() { - local found_agent=false - - # Check each possible agent file and update if it exists - if [[ -f "$CLAUDE_FILE" ]]; then - update_agent_file "$CLAUDE_FILE" "Claude Code" - found_agent=true - fi - - if [[ -f "$GEMINI_FILE" ]]; then - update_agent_file "$GEMINI_FILE" "Gemini CLI" - found_agent=true - fi - - if [[ -f "$COPILOT_FILE" ]]; then - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - found_agent=true - fi - - if [[ -f "$CURSOR_FILE" ]]; then - update_agent_file "$CURSOR_FILE" "Cursor IDE" - found_agent=true - fi - - if [[ -f "$QWEN_FILE" ]]; then - update_agent_file "$QWEN_FILE" "Qwen Code" - found_agent=true - fi - - if [[ -f "$AGENTS_FILE" ]]; then - update_agent_file "$AGENTS_FILE" "Codex/opencode" - found_agent=true - fi - - if [[ -f "$WINDSURF_FILE" ]]; then - update_agent_file "$WINDSURF_FILE" "Windsurf" - found_agent=true - fi - - if [[ -f "$KILOCODE_FILE" ]]; then - update_agent_file "$KILOCODE_FILE" "Kilo Code" - found_agent=true - fi - - if [[ -f "$AUGGIE_FILE" ]]; then - update_agent_file "$AUGGIE_FILE" "Auggie CLI" - found_agent=true - fi - - if [[ -f "$ROO_FILE" ]]; then - update_agent_file "$ROO_FILE" "Roo Code" - found_agent=true - fi - - if [[ -f "$CODEBUDDY_FILE" ]]; then - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" - found_agent=true - fi - - if [[ -f "$SHAI_FILE" ]]; then - update_agent_file "$SHAI_FILE" "SHAI" - found_agent=true - fi - - if [[ -f "$QODER_FILE" ]]; then - update_agent_file "$QODER_FILE" "Qoder CLI" - found_agent=true - fi - - if [[ -f "$KIRO_FILE" ]]; then - update_agent_file "$KIRO_FILE" "Kiro CLI" - found_agent=true - fi - - if [[ -f "$AGY_FILE" ]]; then - update_agent_file "$AGY_FILE" "Antigravity" - found_agent=true - fi - if [[ -f "$BOB_FILE" ]]; then - update_agent_file "$BOB_FILE" "IBM Bob" - found_agent=true - fi - - # If no agent files exist, create a default Claude file - if [[ "$found_agent" == false ]]; then - log_info "No existing agent files found, creating default Claude file..." - update_agent_file "$CLAUDE_FILE" "Claude Code" - fi -} -print_summary() { - echo - log_info "Summary of changes:" - - if [[ -n "$NEW_LANG" ]]; then - echo " - Added language: $NEW_LANG" - fi - - if [[ -n "$NEW_FRAMEWORK" ]]; then - echo " - Added framework: $NEW_FRAMEWORK" - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then - echo " - Added database: $NEW_DB" - fi - - echo - - log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]" -} - -#============================================================================== -# Main Execution -#============================================================================== - -main() { - # Validate environment before proceeding - validate_environment - - log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" - - # Parse the plan file to extract project information - if ! parse_plan_data "$NEW_PLAN"; then - log_error "Failed to parse plan data" - exit 1 - fi - - # Process based on agent type argument - local success=true - - if [[ -z "$AGENT_TYPE" ]]; then - # No specific agent provided - update all existing agent files - log_info "No agent specified, updating all existing agent files..." - if ! update_all_existing_agents; then - success=false - fi - else - # Specific agent provided - update only that agent - log_info "Updating specific agent: $AGENT_TYPE" - if ! update_specific_agent "$AGENT_TYPE"; then - success=false - fi - fi - - # Print summary - print_summary - - if [[ "$success" == true ]]; then - log_success "Agent context update completed successfully" - exit 0 - else - log_error "Agent context update completed with errors" - exit 1 - fi -} - -# Execute main function if script is run directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi +echo "Plan-derived agent context generation is disabled; maintain AGENTS.md directly." diff --git a/.specify/templates/agent-file-template.md b/.specify/templates/agent-file-template.md index 4cc7fd667..71420a59c 100644 --- a/.specify/templates/agent-file-template.md +++ b/.specify/templates/agent-file-template.md @@ -1,28 +1,6 @@ -# [PROJECT NAME] Development Guidelines +# [PROJECT NAME] Agent Instructions -Auto-generated from all feature plans. Last updated: [DATE] - -## Active Technologies - -[EXTRACTED FROM ALL PLAN.MD FILES] - -## Project Structure - -```text -[ACTUAL STRUCTURE FROM PLANS] -``` - -## Commands - -[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] - -## Code Style - -[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE] - -## Recent Changes - -[LAST 3 FEATURES AND WHAT THEY ADDED] +Repository-wide instructions are maintained in `AGENTS.md`. Keep this file feature-neutral; feature plans and historical change summaries belong in `specs/`. diff --git a/AGENTS.md b/AGENTS.md index 52e58fcd9..2f8da3ddc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,28 +81,7 @@ Before handing off changes, verify the following when applicable: - New code follows nullable annotations and existing style. - No unrelated files were changed. - -For additional context about technologies to be used, project structure, -shell commands, and other important information, read `specs/008-weaver-ai-copilot/plan.md`. - +## Instruction Maintenance -## Active Technologies -- C# latest, nullable reference types enabled, implicit usings enabled. + `Microsoft.Extensions.Logging`, `Microsoft.AspNetCore.SignalR`, Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, existing Elsa identity/authorization features. (003-live-server-logs) -- Bounded in-memory ring buffer for MVP; no EF Core schema changes. Provider abstraction allows external/shared log backends later. (003-live-server-logs) -- C# latest, nullable reference types enabled, implicit usings enabled. + `Microsoft.Extensions.Logging`, `Microsoft.Extensions.Options`, `Microsoft.AspNetCore.SignalR`, Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, CShells shell feature infrastructure. (004-diagnostics-structured-logs) -- Existing bounded in-memory ring buffer; no EF Core schema changes. Provider abstraction remains available for future shared backends. (004-diagnostics-structured-logs) -- C# latest, nullable reference types enabled, implicit usings enabled. + Existing `Elsa.Diagnostics.StructuredLogs`, `Microsoft.Extensions.Logging`, `Microsoft.Extensions.Options`, `Microsoft.AspNetCore.SignalR`, Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, FluentMigrator runner packages, SQLite ADO.NET provider, and optionally Dapper for relational operations. (005-structured-log-persistence) -- Bounded in-memory store by default; opt-in SQLite durable store through shared relational persistence. SQLite stores `Timestamp` and `ReceivedAt` as UTC ISO-8601 text and stores exception, scope, and property payloads as JSON text. (005-structured-log-persistence) -- C# latest, nullable reference types enabled, implicit usings enabled. + `Microsoft.Extensions.Options`, `Microsoft.AspNetCore.SignalR`, Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, Elsa shell feature infrastructure, and existing Elsa identity/authorization patterns. (006-diagnostics-console-logs) -- Bounded in-memory recent buffer and bounded subscriber queues by default; no durable database schema. Providers receive redacted content only. (006-diagnostics-console-logs) -- C# latest, nullable reference types enabled, implicit usings enabled. + Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, existing Elsa identity/authorization patterns, Elsa workflow input metadata, `Microsoft.Extensions.Configuration`, `Microsoft.AspNetCore.DataProtection`, EF Core persistence infrastructure, mediator notifications, and optional JavaScript expression integration. (007-secrets-module) -- In-memory store for tests/development; Elsa-managed encrypted store with EF Core persistence for production; configuration-backed read-only store for deployment-managed values. No cloud vault or OS certificate store provider in v1. (007-secrets-module) -- C# latest, nullable reference types enabled, implicit usings enabled; paired Studio Blazor/Razor module work in the Studio repository. + Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, existing identity/authorization and tenancy services, workflow definition/instance abstractions, diagnostics/log abstractions, `Microsoft.Extensions.Options`, `Microsoft.Extensions.Logging`, OpenTelemetry, `System.Text.Json`, SignalR or SSE streaming, GitHub Copilot SDK isolated behind `Elsa.AI.Copilot`, and headless Copilot CLI JSON-RPC integration. (008-weaver-ai-copilot) -- Configurable conversation/session retention with in-memory support for development and tests; durable proposal and audit stores required for MVP using Elsa persistence provider abstractions and an EF Core provider package for production. (008-weaver-ai-copilot) - -## Recent Changes -- 008-weaver-ai-copilot: Captures Weaver as a server-hosted, provider-isolated AI copilot platform with Studio chat, governed tools, proposal-only workflow mutations, audit, and extensibility. -- 006-diagnostics-console-logs: Plans raw stdout/stderr console capture with redaction-before-provider boundaries, bounded in-memory recent/live buffers, REST backfill/source endpoints, and a SignalR live hub. -- 005-structured-log-persistence: Plans pluggable structured log storage with in-memory default and opt-in SQLite persistence using FluentMigrator. -- 004-diagnostics-structured-logs: Refactors the unpublished server logs module into diagnostics structured logs and preserves bounded structured `ILogger` capture. -- 003-live-server-logs: Added C# latest, nullable reference types enabled, implicit usings enabled. + `Microsoft.Extensions.Logging`, `Microsoft.AspNetCore.SignalR`, Elsa feature/module infrastructure, FastEndpoints through Elsa API endpoint patterns, existing Elsa identity/authorization features. +- Keep this file limited to stable, repository-wide guidance. Feature-specific plans and historical change summaries belong in `specs/`, not here. +- Agent-specific instruction files should defer to this file instead of duplicating or generating plan-derived content. diff --git a/CLAUDE.md b/CLAUDE.md index d64573116..4f18cfcd7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,36 +1,3 @@ -# main Development Guidelines +# Elsa Core Agent Instructions -Auto-generated from all feature plans. Last updated: 2026-04-24 - -## Active Technologies - -- C# latest (`latest`), nullable reference types enabled, implicit usings enabled — per `src/Directory.Build.props`. + `Elsa.Workflows.Runtime`, `Elsa.Workflows.Runtime.Distributed`, `Elsa.Hosting.Management` (existing heartbeat), `Elsa.Http` and `Elsa.Scheduling` (first ingress-source adapters), `Elsa.Api.Common` (`ElsaEndpoint` on FastEndpoints), `Elsa.Features` (`IShellFeature`), `Microsoft.Extensions.DependencyInjection`, `Elsa.Mediator`. (002-graceful-shutdown) - -## Project Structure - -```text -src/ -tests/ -``` - -## Commands - -# Add commands for C# latest (`latest`), nullable reference types enabled, implicit usings enabled — per `src/Directory.Build.props`. - -## Code Style - -C# latest (`latest`), nullable reference types enabled, implicit usings enabled — per `src/Directory.Build.props`.: Follow standard conventions - -## Recent Changes - -- 002-graceful-shutdown: Added C# latest (`latest`), nullable reference types enabled, implicit usings enabled — per `src/Directory.Build.props`. + `Elsa.Workflows.Runtime`, `Elsa.Workflows.Runtime.Distributed`, `Elsa.Hosting.Management` (existing heartbeat), `Elsa.Http` and `Elsa.Scheduling` (first ingress-source adapters), `Elsa.Api.Common` (`ElsaEndpoint` on FastEndpoints), `Elsa.Features` (`IShellFeature`), `Microsoft.Extensions.DependencyInjection`, `Elsa.Mediator`. - - -## Agent Operating Principles - -- Do not assume, hide confusion, or flatten uncertainty; surface questions, constraints, and tradeoffs explicitly. -- Write the minimum code that solves the defined problem; do not add speculative abstractions, features, or cleanup. -- Touch only the files and behavior required for the task; clean up only issues introduced by your own changes. -- Define success criteria before implementation, then iterate until the criteria are verified or clearly state what could not be verified. - - +Use the canonical repository guidance in [AGENTS.md](AGENTS.md). Keep feature-specific plan details in `specs/`; they are not copied into this file. From 5be9fe08e9fe20bac7fa03a1ec5a9c9340e4c0e9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jul 2026 00:08:42 +0200 Subject: [PATCH 28/33] Updated AGENTS.md --- .agents/skills/speckit-plan/SKILL.md | 2 +- .claude/skills/speckit-plan/SKILL.md | 2 +- .github/agents/speckit.plan.agent.md | 2 +- .specify/integrations/claude.manifest.json | 6 ++-- .../claude/scripts/update-context.ps1 | 24 ++-------------- .../claude/scripts/update-context.sh | 28 +++---------------- .specify/integrations/codex.manifest.json | 2 +- AGENTS.md | 1 + 8 files changed, 15 insertions(+), 52 deletions(-) diff --git a/.agents/skills/speckit-plan/SKILL.md b/.agents/skills/speckit-plan/SKILL.md index b761b014b..2ae11bd7b 100644 --- a/.agents/skills/speckit-plan/SKILL.md +++ b/.agents/skills/speckit-plan/SKILL.md @@ -62,7 +62,7 @@ You **MUST** consider the user input before proceeding (if not empty). - Evaluate gates (ERROR if violations unjustified) - Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION) - Phase 1: Generate data-model.md, contracts/, quickstart.md - - Phase 1: Update agent context by running the agent script + - Phase 1: Leave repository and agent instruction files unchanged; keep feature context in the plan artifacts - Re-evaluate Constitution Check post-design 4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts. diff --git a/.claude/skills/speckit-plan/SKILL.md b/.claude/skills/speckit-plan/SKILL.md index e272afe71..6704c6a91 100644 --- a/.claude/skills/speckit-plan/SKILL.md +++ b/.claude/skills/speckit-plan/SKILL.md @@ -63,7 +63,7 @@ You **MUST** consider the user input before proceeding (if not empty). - Evaluate gates (ERROR if violations unjustified) - Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION) - Phase 1: Generate data-model.md, contracts/, quickstart.md - - Phase 1: Update agent context by running the agent script + - Phase 1: Leave repository and agent instruction files unchanged; keep feature context in the plan artifacts - Re-evaluate Constitution Check post-design 4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts. diff --git a/.github/agents/speckit.plan.agent.md b/.github/agents/speckit.plan.agent.md index 48ac6695f..d18cfe1e4 100644 --- a/.github/agents/speckit.plan.agent.md +++ b/.github/agents/speckit.plan.agent.md @@ -30,7 +30,7 @@ You **MUST** consider the user input before proceeding (if not empty). - Evaluate gates (ERROR if violations unjustified) - Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION) - Phase 1: Generate data-model.md, contracts/, quickstart.md - - Phase 1: Update agent context by running the agent script + - Phase 1: Leave repository and agent instruction files unchanged; keep feature context in the plan artifacts - Re-evaluate Constitution Check post-design 4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts. diff --git a/.specify/integrations/claude.manifest.json b/.specify/integrations/claude.manifest.json index 308fba369..15787c136 100644 --- a/.specify/integrations/claude.manifest.json +++ b/.specify/integrations/claude.manifest.json @@ -8,11 +8,11 @@ ".claude/skills/speckit-clarify/SKILL.md": "1a77bd8c24d8dcfa8a883185480ef8a1ee0b68e4059ebe6dac3e66e872407ea5", ".claude/skills/speckit-constitution/SKILL.md": "86fd32ace9f5e99b44c6247629f2ef2ddec707459182870b65541e9def309f98", ".claude/skills/speckit-implement/SKILL.md": "bba51e30382cdfab8ef97ba924b7a7b879e0a5ac6733e97f14234e5e188ee42b", - ".claude/skills/speckit-plan/SKILL.md": "e4b7e372a06a987bb20d2142aea40460525363c2ad89a65fc050a67d6bb45eec", + ".claude/skills/speckit-plan/SKILL.md": "f75c339c468a12eeca8ba4a3a3bcc9b3466c2f48e3de25d9c23abdd7bcab4f45", ".claude/skills/speckit-specify/SKILL.md": "69ac96ba89ae6832c67e4c8c75393b7d50ef5a75c766e0261205040e9d3a4850", ".claude/skills/speckit-tasks/SKILL.md": "67c4419fc40299c9ebd68f677fad362dac7de1041df1bfe14b1d4e5e0adade9d", ".claude/skills/speckit-taskstoissues/SKILL.md": "8a8052fd7489424f9d62cc809042b2a2968ccb231466757aeca5bf47d5747222", - ".specify/integrations/claude/scripts/update-context.ps1": "8bce5081fe27ebf414d4eaf127d91b5540b00d24dde4fe1e303e8eb26ad5211a", - ".specify/integrations/claude/scripts/update-context.sh": "21a5aa3fc644f693a29d35975ce21e5a949cdc1d0258b11c21940754c3644fa6" + ".specify/integrations/claude/scripts/update-context.ps1": "e97ba404738b7f5da9f7912b6392e53ac28afa51cf4f925f6c67e01d29b1dde5", + ".specify/integrations/claude/scripts/update-context.sh": "c11f50a2744b0339cc21b886c7bd10e916fbfe876123eb99ec903af083145f43" } } diff --git a/.specify/integrations/claude/scripts/update-context.ps1 b/.specify/integrations/claude/scripts/update-context.ps1 index 837974d47..f77d86e55 100644 --- a/.specify/integrations/claude/scripts/update-context.ps1 +++ b/.specify/integrations/claude/scripts/update-context.ps1 @@ -1,23 +1,5 @@ -# update-context.ps1 — Claude Code integration: create/update CLAUDE.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. +# Retained as a Claude integration compatibility entry point. +# Plan-derived agent context generation is intentionally disabled. $ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType claude +Write-Output 'Plan-derived agent context generation is disabled; maintain AGENTS.md directly.' diff --git a/.specify/integrations/claude/scripts/update-context.sh b/.specify/integrations/claude/scripts/update-context.sh index 4b83855a2..fbd8821e3 100755 --- a/.specify/integrations/claude/scripts/update-context.sh +++ b/.specify/integrations/claude/scripts/update-context.sh @@ -1,28 +1,8 @@ #!/usr/bin/env bash -# update-context.sh — Claude Code integration: create/update CLAUDE.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. + +# Retained as a Claude integration compatibility entry point. +# Plan-derived agent context generation is intentionally disabled. set -euo pipefail -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" claude +echo "Plan-derived agent context generation is disabled; maintain AGENTS.md directly." diff --git a/.specify/integrations/codex.manifest.json b/.specify/integrations/codex.manifest.json index 82a983992..6d33ae195 100644 --- a/.specify/integrations/codex.manifest.json +++ b/.specify/integrations/codex.manifest.json @@ -8,7 +8,7 @@ ".agents/skills/speckit-clarify/SKILL.md": "a429d1af7bf00c65c6be2766a11c2571e9d18ede2b77ec4fd538db4d5ec242d1", ".agents/skills/speckit-constitution/SKILL.md": "7310adee465ee6a439a689518e6c133ce851d808c7a6e64d66f659ad8b4bd56e", ".agents/skills/speckit-implement/SKILL.md": "b39c4de2e794a96302cd36cc7b6796711aee0f3bcedba0d0c01ccf3b1036f898", - ".agents/skills/speckit-plan/SKILL.md": "1cd13274eb35a18d87e6f43bc69801910437ce9543286b13f87c4e3cbc33b9d8", + ".agents/skills/speckit-plan/SKILL.md": "d0033856f26b7e5ff683f181ceb20de7ca4f247c290b2c76241719be40e09012", ".agents/skills/speckit-specify/SKILL.md": "164542c78d6415ea9e16bb063652e989e14524a9a273176273e6c9b5ffa0b942", ".agents/skills/speckit-tasks/SKILL.md": "0b36b19eb61347406afea1953c1e5983f084931eaa34d1b10c6c6e67f828f29f", ".agents/skills/speckit-taskstoissues/SKILL.md": "32d8ee0f0482a7b2b9e6bee4dfd6947465754f2f82c404bfc7ef64f4a3b63bdd" diff --git a/AGENTS.md b/AGENTS.md index 2f8da3ddc..8e81976a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,3 +85,4 @@ Before handing off changes, verify the following when applicable: - Keep this file limited to stable, repository-wide guidance. Feature-specific plans and historical change summaries belong in `specs/`, not here. - Agent-specific instruction files should defer to this file instead of duplicating or generating plan-derived content. +- Follow the global Codex workroom model and fallback policy for orchestration and delegation. That policy is authoritative for model order and for distinguishing model unavailability from delegation timeout or failure; do not duplicate its details here. From aef7f253c3193d8f4e291fa1e0985bfa26463d6d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 22 Jul 2026 00:07:49 +0200 Subject: [PATCH 29/33] docs: refresh roadmap --- ROADMAP.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 480011a6f..297dcc64b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Elsa Roadmap -Last refreshed: 2026-07-01 +Last refreshed: 2026-07-22 This roadmap is a product direction document, not a fixed release calendar. Elsa is developed through a mix of core maintainer work, customer-funded work, and community contributions, so sequencing can change when real-world demand changes. The intent is stable: make Elsa the most productive, dependable, and extensible workflow platform for the .NET ecosystem. @@ -40,6 +40,7 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio - [x] Console logs - [x] Studio structured-log, console-log, and OpenTelemetry diagnostics foundations - [x] Durable structured log persistence +- [~] Operational dashboard API and Studio dashboard productization - [~] Scheduler and message-bus foundations through Quartz, Hangfire, MassTransit, Kafka, and Azure Service Bus - [~] OpenTelemetry diagnostics backend and default workflow metrics - [ ] Scheduler/message reliability hardening for clustered production workloads @@ -73,6 +74,7 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio - [x] HTTP, scheduling, scripting, and persistence provider foundations - [x] Connections and Secrets foundations - [x] Extension packages for SQL, CSV, Email, Slack, Telnyx, GitHub DevOps, Azure Storage, Azure Service Bus, Kafka, MassTransit/RabbitMQ, Quartz, Hangfire, Dapper, MongoDB, Elasticsearch, OpenTelemetry, Logging, Agents, OpenAPI, Webhooks, OrchardCore, IO, compression, and ProtoActor-backed runtime/caching +- [~] MQTT publish and message-trigger foundation (main-only; release and production hardening pending) - [~] Modular package loading and manifest metadata - [~] OpenAPI activity/provider foundations - [ ] Connector SDK @@ -101,6 +103,7 @@ These are already present in the codebase and should be treated as foundations f - Structured diagnostics with recent/live capture plus SQLite persistence in [`Elsa.Diagnostics.StructuredLogs`](src/modules/Elsa.Diagnostics.StructuredLogs) and [`Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite`](src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite). - Raw stdout/stderr console diagnostics in [`Elsa.Diagnostics.ConsoleLogs`](src/modules/Elsa.Diagnostics.ConsoleLogs), with the post-3.7 console pipeline now carrying workflow and activity execution context through [PR #7536](https://github.com/elsa-workflows/elsa-core/pull/7536). - Core `main` now includes `Elsa.Diagnostics.OpenTelemetry`, which provides OTLP ingestion, bounded in-memory storage, REST APIs, SignalR live updates, collector configuration, permissions, and tests in [`src/modules/Elsa.Diagnostics.OpenTelemetry`](src/modules/Elsa.Diagnostics.OpenTelemetry). The productization gap is no longer "does a backend exist?" but rather release packaging, default workflow semantic metrics, and diagnostics correlation. +- Core `main` includes [`Elsa.Dashboard.Api`](src/modules/Elsa.Dashboard.Api), a read-only operational-dashboard backend with overview, trends, attention, recent-activity, and workflow-hotspot endpoints plus independent diagnostics capability states ([#7529](https://github.com/elsa-workflows/elsa-core/pull/7529), [#7532](https://github.com/elsa-workflows/elsa-core/pull/7532)). Its remaining product work is a released, polished Studio dashboard with robust deep links and unavailable-state handling. - Core `main` now includes `Elsa.AI.Abstractions`, `Elsa.AI.Host`, `Elsa.AI.Copilot`, and `Elsa.AI.Persistence.EFCore` through [PR #7523](https://github.com/elsa-workflows/elsa-core/pull/7523), and Studio `main` now includes `Elsa.Studio.AI` through [elsa-studio#900](https://github.com/elsa-workflows/elsa-studio/pull/900). That gives Weaver both server and Studio workspace foundations, while proposal actions, broader authoring contracts, and polished product UX remain roadmap work. - State machine core activity support in [`Elsa.Workflows.Core/Activities/StateMachine`](src/modules/Elsa.Workflows.Core/Activities/StateMachine). - ElsaScript DSL and blob storage integration in [`Elsa.Dsl.ElsaScript`](src/modules/Elsa.Dsl.ElsaScript) and [`Elsa.WorkflowProviders.BlobStorage.ElsaScript`](src/modules/Elsa.WorkflowProviders.BlobStorage.ElsaScript). @@ -112,6 +115,7 @@ These are already present in the codebase and should be treated as foundations f - Elsa Extensions is an active modular integration repository with 70+ module projects in [elsa-workflows/elsa-extensions](https://github.com/elsa-workflows/elsa-extensions), targeting `net8.0`, `net9.0`, and `net10.0`. - Extensions already provide broad integration foundations: Connections, Secrets, Agents, OpenAPI, SQL/CSV/data tooling, messaging, schedulers, cloud storage, logging, webhooks, persistence providers, LDAP, and external system activities. - Extensions `3.7.0` adds package manifest metadata, infrastructure attributes, shell features for MassTransit/Quartz/Webhooks, Dapper and MongoDB activity execution-chain lookups, Dapper bookmark queue filtering, Kafka multitenancy/schema-trigger work, Quartz lifecycle/job cleanup fixes, and other operational hardening. +- Extensions `main` now includes an MQTT module with publish and message-received trigger activities, broker/TLS/QoS configuration, Studio UI hints, and focused tests ([elsa-extensions#157](https://github.com/elsa-workflows/elsa-extensions/pull/157)). It remains a main-only foundation; its documented MQTT 5, advanced TLS, binary-payload, and error/DLQ gaps keep release and operational productization on the roadmap. The public roadmap issue remains useful history: [elsa-workflows/elsa-core#3232](https://github.com/elsa-workflows/elsa-core/issues/3232). Several items in that issue are now done in code but still open in the issue body, so this file should be considered the current working roadmap. @@ -145,7 +149,7 @@ Recommended success measures: High-value items: - Ship workflow organization as a coherent feature: labels/categories, folder-like views, search/filter by metadata, and Studio support. This consolidates [#5872](https://github.com/elsa-workflows/elsa-core/issues/5872), [#6307](https://github.com/elsa-workflows/elsa-core/issues/6307), the existing `Elsa.Labels` module, and workflow definition `CustomProperties`. -- Make designer reliability a visible workstream. Recent Studio issues show expression/input rendering regressions after 3.6 ([elsa-studio#791](https://github.com/elsa-workflows/elsa-studio/issues/791), [elsa-studio#781](https://github.com/elsa-workflows/elsa-studio/issues/781), [elsa-studio#795](https://github.com/elsa-workflows/elsa-studio/issues/795)); these should drive a regression harness for designer rendering, property editors, expression descriptors, drag/drop, and WASM/Server parity. +- Make designer reliability a visible workstream. Recent Studio issues show expression/input rendering regressions after 3.6 ([elsa-studio#791](https://github.com/elsa-workflows/elsa-studio/issues/791), [elsa-studio#781](https://github.com/elsa-workflows/elsa-studio/issues/781), [elsa-studio#795](https://github.com/elsa-workflows/elsa-studio/issues/795)), persistent browser/JS failures ([elsa-studio#903](https://github.com/elsa-workflows/elsa-studio/issues/903)), and fragile Playwright automation ([elsa-studio#919](https://github.com/elsa-workflows/elsa-studio/issues/919)). These should drive a regression harness for designer rendering, property editors, expression descriptors, drag/drop, stable test identifiers, deterministic canvas state, and WASM/Server parity. - Make workflow progress visible to application users: a current-state/step API, timeline model, and embeddable progress component. This responds to [discussion #6012](https://github.com/elsa-workflows/elsa-core/discussions/6012) and should reuse execution logs, activity records, call-stack tracking, and real-time workflow updates. - Finish the state machine product surface. The core activity exists, but [#5085](https://github.com/elsa-workflows/elsa-core/issues/5085) should be closed only when JSON serialization, Studio authoring, docs, and examples make state machines approachable. - Build first-class workflow testing and debugging: test runners for full workflows, breakpoint-like inspection, replay from execution logs where feasible, better failed-activity retry flows, child/descendant workflow instance navigation, and Studio affordances for fault investigation. The Studio `3.7.0` activity call-stack viewer is a useful foundation, but requests for child workflow visibility ([elsa-studio#152](https://github.com/elsa-workflows/elsa-studio/issues/152)) and breakpoint debugging ([elsa-studio discussion #662](https://github.com/elsa-workflows/elsa-studio/discussions/662)) still need a coherent debugging experience. From 04e58690ff46e2b812b742e5a19079b780b74243 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 26 Jul 2026 21:42:27 +0200 Subject: [PATCH 30/33] chore: apply safe dependency upgrades (#7896) --- Directory.Packages.props | 131 +- build/_build.csproj | 2 +- .../video/elsa-readme-video/package-lock.json | 1675 +++++++++++++---- design/video/elsa-readme-video/package.json | 8 +- ...s.StructuredLogs.Persistence.Sqlite.csproj | 1 + .../Elsa.Persistence.EFCore.Sqlite.csproj | 3 +- .../Elsa.Persistence.VNext.Sqlite.csproj | 1 + .../Elsa.AI.IntegrationTests.csproj | 1 + ...lsa.AI.Persistence.EFCore.UnitTests.csproj | 1 + .../Elsa.Persistence.VNext.UnitTests.csproj | 1 + 10 files changed, 1360 insertions(+), 464 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a21b268c1..a395ef2a0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,39 +8,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - + + + @@ -50,39 +50,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - + + + @@ -141,7 +141,7 @@ - + @@ -181,13 +181,14 @@ + - + - + diff --git a/build/_build.csproj b/build/_build.csproj index de1b44c7b..f41dc9192 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -21,7 +21,7 @@ - + diff --git a/design/video/elsa-readme-video/package-lock.json b/design/video/elsa-readme-video/package-lock.json index 4810e9cf9..7297002d6 100644 --- a/design/video/elsa-readme-video/package-lock.json +++ b/design/video/elsa-readme-video/package-lock.json @@ -8,10 +8,10 @@ "name": "elsa-readme-video", "version": "0.1.0", "dependencies": { - "@remotion/cli": "4.0.469", - "react": "19.2.6", - "react-dom": "19.2.6", - "remotion": "4.0.469" + "@remotion/cli": "4.0.499", + "react": "19.2.8", + "react-dom": "19.2.8", + "remotion": "4.0.499" }, "devDependencies": { "@types/react": "19.2.14", @@ -19,6 +19,217 @@ "typescript": "6.0.3" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -37,6 +248,41 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", @@ -49,6 +295,94 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", @@ -64,20 +398,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, "dependencies": { @@ -85,9 +419,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "license": "MIT", "optional": true, "dependencies": { @@ -95,9 +429,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -111,9 +445,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -127,9 +461,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -143,9 +477,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -159,9 +493,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -175,9 +509,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -191,9 +525,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -207,9 +541,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -223,9 +557,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -239,9 +573,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -255,9 +589,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -271,9 +605,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -287,9 +621,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -303,9 +637,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -319,9 +653,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -335,9 +669,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -351,9 +685,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -367,9 +701,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -383,9 +717,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -399,9 +733,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -415,9 +749,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -431,9 +765,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -447,9 +781,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -463,9 +797,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -479,9 +813,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -495,9 +829,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -520,6 +854,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -556,9 +900,9 @@ } }, "node_modules/@mediabunny/aac-encoder": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.45.0.tgz", - "integrity": "sha512-vLQw8cY7Me6pvTTMkMhOiH9UCuINzfTOETCeDxbGNeNfDqc/7QlxloUH1Ylp/Zz2ek0O8kc6YdygV2vWAPakrA==", + "version": "1.50.8", + "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.50.8.tgz", + "integrity": "sha512-A5Se/LZd6RmYq/h36lBMSEsHvsyW8d0toR7FrAwpsFYbK+DVQYf90KiBT1Aw/mzLXx8/ypIOORJnd1sVZqOvJQ==", "license": "MPL-2.0", "funding": { "type": "individual", @@ -569,9 +913,9 @@ } }, "node_modules/@mediabunny/flac-encoder": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.45.0.tgz", - "integrity": "sha512-LfKbAMZVkxRS7PpEIVnWOY/l0KcHv+rjO7pYY3O0TPCZvbHWfrnQjn8JPacPIfuq6Yv7r4f8lhcl7yHSynoRkQ==", + "version": "1.50.8", + "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.50.8.tgz", + "integrity": "sha512-4cfN03SbEoQaG+eBeYFUAb1R1ALaAHzf43dXFzQTr/oO5ueqzTgBoG3coKrIvBaAMU7Vv36mrBKLX8bvTppdAQ==", "license": "MPL-2.0", "funding": { "type": "individual", @@ -582,9 +926,9 @@ } }, "node_modules/@mediabunny/mp3-encoder": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.45.0.tgz", - "integrity": "sha512-Bobi6AaQYEc7TWmPJ8Q0/hcUtBN7pLUC2qjoC7oZR4FcGqGztby6k7A1SWlmswoMOEIhYsOrgDaemrSDAC0QVQ==", + "version": "1.50.8", + "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.50.8.tgz", + "integrity": "sha512-eBT/H30tTu8AmZXqZ5RTpJ9VmwVlwednajuOrrWp0GS6cbGXsGMQ60Qvr3ZMIE0QDiadlEzb8/ozR+doP+MC1g==", "license": "MPL-2.0", "funding": { "type": "individual", @@ -660,21 +1004,21 @@ } }, "node_modules/@remotion/bundler": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.469.tgz", - "integrity": "sha512-vHJD/Ey3aTmjlFJ2m9stA+BK3uoacjNB7dT0hSSFQjuN0X4suNmSgA3lRhxPBcUFrrziy4HRXaiJp5H2y3Yc2Q==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.499.tgz", + "integrity": "sha512-88C7WxlC9DSxEOSnJL/HV7Xl16OIKnEzl90BvIM083pNBA/laAEJvryaXmSQ3FuI5CSGuVA336YgboOWagqmAg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@remotion/media-parser": "4.0.469", - "@remotion/studio": "4.0.469", - "@remotion/studio-shared": "4.0.469", - "@remotion/timeline-utils": "4.0.469", - "@rspack/core": "1.7.6", + "@remotion/media-parser": "4.0.499", + "@remotion/studio": "4.0.499", + "@remotion/studio-shared": "4.0.499", + "@remotion/timeline-utils": "4.0.499", + "@rspack/core": "1.7.11", "@rspack/plugin-react-refresh": "1.6.1", "css-loader": "7.1.4", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "react-refresh": "0.18.0", - "remotion": "4.0.469", + "remotion": "4.0.499", "style-loader": "4.0.0", "webpack": "5.105.0" }, @@ -683,23 +1027,37 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@remotion/canvas-capture": { + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/canvas-capture/-/canvas-capture-4.0.499.tgz", + "integrity": "sha512-S2Q5eqNnjkRmxm5FZMadI21bPuLyKa2+zRLh3c2EDjRS1C400rx19UCmG1Ne9W38ybInm/MohcLS3EkaNHZqrw==", + "license": "Remotion License", + "dependencies": { + "mediabunny": "1.50.8", + "remotion": "4.0.499" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remotion/cli": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.469.tgz", - "integrity": "sha512-dv3hpY0CsjzkwPVXQJcJQHikXLHzupFyJg835rp5zBuqF+i/HmhZM8VZdh4LZDEut3OHJD3vkEgAk35R18Q8iQ==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.499.tgz", + "integrity": "sha512-IOscAgjRECQhwUaXEFBc3x6FvRaEcQNyx3qGxyprPL+K3xwmfTVJgYI8igMQZUgEUxtwdDQ2hjMUB+30wgxa1g==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@remotion/bundler": "4.0.469", - "@remotion/media-utils": "4.0.469", - "@remotion/player": "4.0.469", - "@remotion/renderer": "4.0.469", - "@remotion/studio": "4.0.469", - "@remotion/studio-server": "4.0.469", - "@remotion/studio-shared": "4.0.469", + "@remotion/bundler": "4.0.499", + "@remotion/media-utils": "4.0.499", + "@remotion/player": "4.0.499", + "@remotion/renderer": "4.0.499", + "@remotion/studio": "4.0.499", + "@remotion/studio-server": "4.0.499", + "@remotion/studio-shared": "4.0.499", "dotenv": "17.3.1", "minimist": "1.2.6", "prompts": "2.4.2", - "remotion": "4.0.469" + "remotion": "4.0.499" }, "bin": { "remotion": "remotion-cli.js", @@ -712,9 +1070,9 @@ } }, "node_modules/@remotion/compositor-darwin-arm64": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.469.tgz", - "integrity": "sha512-Ke/PljsHgvHIJD7rGlgTGkvmY044abjTUo2QI3Qq368NGzzEHe2USVyqycacKTilHCtcZG1n8CMbofpkXHurcg==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.499.tgz", + "integrity": "sha512-7pBC1NwnzFFHRmCIx0DlRMb53SIwGb24Abx28f3wSdrgYVAU9A0Ema/Y+h0dZVGIwC3RO0iUQgs3h8ra7XVB6Q==", "cpu": [ "arm64" ], @@ -724,9 +1082,9 @@ ] }, "node_modules/@remotion/compositor-darwin-x64": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.469.tgz", - "integrity": "sha512-e6usJHMZQN1qUrjLfaVXAHk212W+FMDzDQWJAnxzXDnb+diPpFvzmW8tGdZWTOjU5mBttEJQmS507zHvs2q4uQ==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.499.tgz", + "integrity": "sha512-gFGYRH7CRIqhOuxQDGnIoHCEMhUeu7zJWuA/fy0hPPnWayDMsWChfY9+yGHsWGBePbNdXEM2HBcuC4RxDLORtQ==", "cpu": [ "x64" ], @@ -736,9 +1094,9 @@ ] }, "node_modules/@remotion/compositor-linux-arm64-gnu": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.469.tgz", - "integrity": "sha512-/N7Bhq024Fw10n6kvx53D8EO3UQOOL6gmq8PJ3o7XORDSANzyYtTogyqHFMJ7LxZ5MLRP1vbdu2kqimBYJA3qw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.499.tgz", + "integrity": "sha512-aLSwT8857xWuPaYFDSOVPSVp6efXnPYS8MEUdKQdCoqXNpgufugCAGL/xzagPo2A3wD+Ocuzj6+bcsBna3ivMg==", "cpu": [ "arm64" ], @@ -751,9 +1109,9 @@ ] }, "node_modules/@remotion/compositor-linux-arm64-musl": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.469.tgz", - "integrity": "sha512-kVlqB1SSJ/ihVncgwEh9uKqMhjdtS+6NTuUTpZbVlEx34s8SN7Y7MV0cHHQnx4KiksbFvVABGVjL9a6yiqIuEw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.499.tgz", + "integrity": "sha512-bC3/NocB8FUiieUBZtE2A19sr34cLDHo5r0Q1aUevsI/RpJkq9LYJzmifMgO+8OxVxv4nxg8cSryHrDpEsylZQ==", "cpu": [ "arm64" ], @@ -766,9 +1124,9 @@ ] }, "node_modules/@remotion/compositor-linux-x64-gnu": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.469.tgz", - "integrity": "sha512-lP+Hzujpk3IB41thQEAf3vnK3bhfqjPrxytKqQ4gWxV830mhQ7g3Htgu9oWGYfshdbfWeMjlRF8rEClzcU/xBw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.499.tgz", + "integrity": "sha512-I9aOX1tpdSwi+jwYcZjg0V8jnp0eVKBWe6iaD3V85USrxxFu4LdvHwJ+Lh6kWC7L1mlrlrac9AGkZm70AoBvWQ==", "cpu": [ "x64" ], @@ -781,9 +1139,9 @@ ] }, "node_modules/@remotion/compositor-linux-x64-musl": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.469.tgz", - "integrity": "sha512-5NV/W1rmZIQFn4ivYCNyg/YooUxKkd+EX/d+rdt/sidPzQUpySRm81F+pnnuqnVldlHmr6LGl9sNe5DSH9dJtw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.499.tgz", + "integrity": "sha512-pDBDfyuaVBueuIqd88SSdCerSzixm3toZZfLbS4ACxLUqtGJshRcmuFeWiaUALSbtwa4KIPMPzgPGCu5cmnuRA==", "cpu": [ "x64" ], @@ -796,9 +1154,9 @@ ] }, "node_modules/@remotion/compositor-win32-x64-msvc": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.469.tgz", - "integrity": "sha512-0lEP9HvbCgQa7HxMl804DrE49kmctidNgSNHavVT7O7B4E03uxAt4kaRRy1P7SLeGMJ/1wKIYLfWtvV4sd7HKA==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.499.tgz", + "integrity": "sha512-sOYpHz3LmW7u0JR3qg3FA2fTP07pEzY6lhKutwTUDAoQ7MJaHdRYBU7DGf/dpBbaGugQ36hi9GKh0jJLbt1aWw==", "cpu": [ "x64" ], @@ -807,26 +1165,32 @@ "win32" ] }, + "node_modules/@remotion/drag-and-drop": { + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/drag-and-drop/-/drag-and-drop-4.0.499.tgz", + "integrity": "sha512-0BBcRlRvkfbTo/ZhLLCOlaXvtFGO4Am6M8YGemEjh/HS7wkIkftpWlLDEEJwNeAlWEa11/EYpgWxFGF3huYOBQ==", + "license": "Remotion License" + }, "node_modules/@remotion/licensing": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.469.tgz", - "integrity": "sha512-ZSM0no+STTcEQAYGyyarXKkpQet18kXgJR59oTmRa0iqS+nN78OMBE4Uj7totpfmhXYV98T4gWgkGHqjDjqcBg==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.499.tgz", + "integrity": "sha512-dKjaYAJsxCUXDvAjJW8bHIUe+dYAXEcq2M0RVrqdS1CEUZPUQXhzIbYILX+slcVb5c8/DD5t+edm7d2VSY81Tg==", "license": "MIT" }, "node_modules/@remotion/media-parser": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.469.tgz", - "integrity": "sha512-mBHh2e7YK3CFiDvfXVoFxgxZrg7PtlDnYKCQHmkoqleBP3rpJlYaXeCgHt+mDwSUd0HFxGFdKYlDdXEn0Kh4kQ==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.499.tgz", + "integrity": "sha512-3RX9OFko0gW1ixi87eoYx2FLOndVb4eo/KONIiq0yKwho1IMYAq8r8tIkqmSksVG0RlWqW4FwXa9kp9lhiqfDA==", "license": "Remotion License https://remotion.dev/license" }, "node_modules/@remotion/media-utils": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.469.tgz", - "integrity": "sha512-L1Qk0PR0BQRhuCgBBnPSKi0zVTe2U2vx8Z2GmNgTqaJvNnzyMJ4WyWyMWYegNs68nFKoUJ5yskkI6oPUwrhCmg==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.499.tgz", + "integrity": "sha512-XqCX58ZwI80YM9CqzasKYeCVGRaINMhRvaCQ4H953PD+pRkTxk3bFcx97x4oj0Y0KjGDWnBvzsa5Tt0Jx7JUBA==", "license": "MIT", "dependencies": { - "mediabunny": "1.45.0", - "remotion": "4.0.469" + "mediabunny": "1.50.8", + "remotion": "4.0.499" }, "peerDependencies": { "react": ">=16.8.0", @@ -834,12 +1198,12 @@ } }, "node_modules/@remotion/player": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.469.tgz", - "integrity": "sha512-cAnin3JPrJx0mBFT7bcZ9N8xOwXygMIWeM4qHgDONfIwgqouir8gQFPIxRtyAzNubMl8xvLjbPSBbDeusNDAGw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.499.tgz", + "integrity": "sha512-3qEjNUj0KamSgWLXDhXnHdVMIx0EBsdr7XM5+QGldj1EiZH54vXVKJsWJj6SYfPbaskfEWI0wjKGGh0ml3vJrw==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "remotion": "4.0.469" + "remotion": "4.0.499" }, "peerDependencies": { "react": ">=16.8.0", @@ -847,26 +1211,26 @@ } }, "node_modules/@remotion/renderer": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.469.tgz", - "integrity": "sha512-9529YcfPbZCQCVhq/eGQaqsuQzAeXClWzq9Eq3vC1UpJrAwsA3DfpDZiwu8IOXVhKqR4uouYJZQagcAahhH9bg==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.499.tgz", + "integrity": "sha512-KqzYTBIYVWuqKd5tfYaBq9CWKRYZJq6/PL5++/20dQaTSHQQEMTpyyGrOs0RsyCiy8Ofr8eYB765mIeIxulS4Q==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@remotion/licensing": "4.0.469", - "@remotion/streaming": "4.0.469", + "@remotion/licensing": "4.0.499", + "@remotion/streaming": "4.0.499", "execa": "5.1.1", - "remotion": "4.0.469", + "remotion": "4.0.499", "source-map": "0.8.0-beta.0", - "ws": "8.20.1" + "ws": "8.21.0" }, "optionalDependencies": { - "@remotion/compositor-darwin-arm64": "4.0.469", - "@remotion/compositor-darwin-x64": "4.0.469", - "@remotion/compositor-linux-arm64-gnu": "4.0.469", - "@remotion/compositor-linux-arm64-musl": "4.0.469", - "@remotion/compositor-linux-x64-gnu": "4.0.469", - "@remotion/compositor-linux-x64-musl": "4.0.469", - "@remotion/compositor-win32-x64-msvc": "4.0.469" + "@remotion/compositor-darwin-arm64": "4.0.499", + "@remotion/compositor-darwin-x64": "4.0.499", + "@remotion/compositor-linux-arm64-gnu": "4.0.499", + "@remotion/compositor-linux-arm64-musl": "4.0.499", + "@remotion/compositor-linux-x64-gnu": "4.0.499", + "@remotion/compositor-linux-x64-musl": "4.0.499", + "@remotion/compositor-win32-x64-msvc": "4.0.499" }, "peerDependencies": { "react": ">=16.8.0", @@ -874,31 +1238,33 @@ } }, "node_modules/@remotion/streaming": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.469.tgz", - "integrity": "sha512-ACYjvcoCxIhVLICgm6zNyxKtXOsRnQ+GocWyY8SdHS2LNpLj/ihRj2xXAdJcuqQI/FaITHIvMiK9kwGAv3J6iw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.499.tgz", + "integrity": "sha512-PYIZc7KNJyG+qvKYzftrPbfVJw5B31F+6Uage0Qf54c+cpa69Us/kFK8quNbd/zZcwxvGvCR+fCkn41TJ53FfQ==", "license": "MIT" }, "node_modules/@remotion/studio": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.469.tgz", - "integrity": "sha512-YMwNokK3XDfhCjZs0J6zj4kcOXlb96YugMMfOY3xBHyVmbKOnSyhNHyZ07E9Kr48p2lorzzS6n6kpFrpOt2FLQ==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.499.tgz", + "integrity": "sha512-oW5hvpwZmstFY9zQZuqAjoAUfujoZna2T9Eq636RwhzDqTaCa3zvV8ojrPfAgFOl8XEFwHLK+UPxAijzOXVYMg==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@remotion/media-utils": "4.0.469", - "@remotion/player": "4.0.469", - "@remotion/renderer": "4.0.469", - "@remotion/studio-shared": "4.0.469", - "@remotion/timeline-utils": "4.0.469", - "@remotion/web-renderer": "4.0.469", - "@remotion/zod-types": "4.0.469", - "mediabunny": "1.45.0", + "@remotion/canvas-capture": "4.0.499", + "@remotion/drag-and-drop": "4.0.499", + "@remotion/media-utils": "4.0.499", + "@remotion/player": "4.0.499", + "@remotion/renderer": "4.0.499", + "@remotion/studio-shared": "4.0.499", + "@remotion/timeline-utils": "4.0.499", + "@remotion/web-renderer": "4.0.499", + "@remotion/zod-types": "4.0.499", + "mediabunny": "1.50.8", "memfs": "3.4.3", "open": "8.4.2", - "remotion": "4.0.469", + "remotion": "4.0.499", "semver": "7.5.3", - "zod": "4.3.6" + "zod": "4.4.3" }, "peerDependencies": { "react": ">=16.8.0", @@ -906,54 +1272,59 @@ } }, "node_modules/@remotion/studio-server": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.469.tgz", - "integrity": "sha512-QnkXtHS6exjHXEQbIK8qXN/8c6jAO6FKrK7917+8E/+0Bl8I18K63yX/CxtONcRuj7qOPfq/s0fIpBO2awczbw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.499.tgz", + "integrity": "sha512-80VI/QY5ysarjbK6gPn6xysIEhha2r1WIbK2mN4TzVSmQXVTB14YPw1noHwCGuNWETj+V8L34AHpAtZp2yxl9Q==", "license": "MIT", "dependencies": { "@babel/parser": "7.24.1", "@babel/types": "7.24.0", - "@remotion/bundler": "4.0.469", - "@remotion/renderer": "4.0.469", - "@remotion/studio-shared": "4.0.469", + "@remotion/bundler": "4.0.499", + "@remotion/drag-and-drop": "4.0.499", + "@remotion/renderer": "4.0.499", + "@remotion/studio-shared": "4.0.499", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "kiwi-schema": "0.5.0", "memfs": "3.4.3", "open": "8.4.2", "prettier": "3.8.1", "recast": "0.23.11", - "remotion": "4.0.469", + "remotion": "4.0.499", "semver": "7.5.3" } }, "node_modules/@remotion/studio-shared": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.469.tgz", - "integrity": "sha512-JN4jdfaLNYLo/aV5jPt13A7dheRm3MuF27Y9ON0Gs/VZva9G+nHyz89zUqMYWLaXU3gG5Sq9HkFa/lGMWC+2Ow==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.499.tgz", + "integrity": "sha512-wGMhqAk8t4UtFneYY8mkUQEWFM95ACQ0Tf87Oh1rPV3RVSN49irQLcBK1eQ8kDM9I9G3oFGQdMYS/6BA8dHnWw==", "license": "MIT", "dependencies": { - "remotion": "4.0.469" + "@remotion/drag-and-drop": "4.0.499", + "remotion": "4.0.499" } }, "node_modules/@remotion/timeline-utils": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.469.tgz", - "integrity": "sha512-Jrl0IUdgljfh64SgOxkFvgMX4UY1nYxDp8Hddrk2R8edIcVUJrMoe2+xLlgFoKwyhyZNE4kHNXnrVlUoSI4d5g==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.499.tgz", + "integrity": "sha512-1ml1W9ia/Aaqauqjj7XRMbY3IMIybzZKZJ7C5tm+Dm8lq2QmCH5QQHi84ToLLVOF8T6m/n+wIZXyhUsNQLh94g==", "license": "MIT", "dependencies": { - "mediabunny": "1.45.0" + "mediabunny": "1.50.8" } }, "node_modules/@remotion/web-renderer": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.469.tgz", - "integrity": "sha512-6Bgx3zLy4AgQiT2ERQCqiBUatNfQhZ4amod6kIy3Xmc8nUw7m2KtzUXZfy7V+Ln3/yo3cRcBa1ehFSRiNbUfCg==", - "license": "UNLICENSED", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.499.tgz", + "integrity": "sha512-wjxJ1wLrzLxO0nTu/1IdRVd3J1kipzatkfkaXwre8gdEwVTg10XD33niufGuD35ykCH4LGOCmkMlhRcXopJ1lA==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@mediabunny/aac-encoder": "1.45.0", - "@mediabunny/flac-encoder": "1.45.0", - "@mediabunny/mp3-encoder": "1.45.0", - "@remotion/licensing": "4.0.469", - "mediabunny": "1.45.0", - "remotion": "4.0.469" + "@mediabunny/aac-encoder": "1.50.8", + "@mediabunny/flac-encoder": "1.50.8", + "@mediabunny/mp3-encoder": "1.50.8", + "@remotion/licensing": "4.0.499", + "mediabunny": "1.50.8", + "remotion": "4.0.499" }, "peerDependencies": { "react": ">=18.0.0", @@ -961,39 +1332,36 @@ } }, "node_modules/@remotion/zod-types": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.469.tgz", - "integrity": "sha512-AfWk/Y4vRepXAsQI41lHwjZyxm0KtNjx04EfTdfvKZq5lcTAEvLcpRlVQhGBazuCY4XXbhsNTSANeOpNAJF2yw==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.499.tgz", + "integrity": "sha512-UyKMhwnS9T6NgHUznscZ4+pPkvjEIqfd6gEVqW6JkM3C7OcZfOBRcF7FWZZq96eyTIxJHLxCUDdlAZr8JcRlTA==", "license": "MIT", "dependencies": { - "remotion": "4.0.469" - }, - "peerDependencies": { - "zod": "4.3.6" + "remotion": "4.0.499" } }, "node_modules/@rspack/binding": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.6.tgz", - "integrity": "sha512-/NrEcfo8Gx22hLGysanrV6gHMuqZSxToSci/3M4kzEQtF5cPjfOv5pqeLK/+B6cr56ul/OmE96cCdWcXeVnFjQ==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz", + "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", "license": "MIT", "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.7.6", - "@rspack/binding-darwin-x64": "1.7.6", - "@rspack/binding-linux-arm64-gnu": "1.7.6", - "@rspack/binding-linux-arm64-musl": "1.7.6", - "@rspack/binding-linux-x64-gnu": "1.7.6", - "@rspack/binding-linux-x64-musl": "1.7.6", - "@rspack/binding-wasm32-wasi": "1.7.6", - "@rspack/binding-win32-arm64-msvc": "1.7.6", - "@rspack/binding-win32-ia32-msvc": "1.7.6", - "@rspack/binding-win32-x64-msvc": "1.7.6" + "@rspack/binding-darwin-arm64": "1.7.11", + "@rspack/binding-darwin-x64": "1.7.11", + "@rspack/binding-linux-arm64-gnu": "1.7.11", + "@rspack/binding-linux-arm64-musl": "1.7.11", + "@rspack/binding-linux-x64-gnu": "1.7.11", + "@rspack/binding-linux-x64-musl": "1.7.11", + "@rspack/binding-wasm32-wasi": "1.7.11", + "@rspack/binding-win32-arm64-msvc": "1.7.11", + "@rspack/binding-win32-ia32-msvc": "1.7.11", + "@rspack/binding-win32-x64-msvc": "1.7.11" } }, "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.6.tgz", - "integrity": "sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz", + "integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==", "cpu": [ "arm64" ], @@ -1004,9 +1372,9 @@ ] }, "node_modules/@rspack/binding-darwin-x64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.6.tgz", - "integrity": "sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz", + "integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==", "cpu": [ "x64" ], @@ -1017,9 +1385,9 @@ ] }, "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.6.tgz", - "integrity": "sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz", + "integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==", "cpu": [ "arm64" ], @@ -1033,9 +1401,9 @@ ] }, "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.6.tgz", - "integrity": "sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz", + "integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==", "cpu": [ "arm64" ], @@ -1049,9 +1417,9 @@ ] }, "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.6.tgz", - "integrity": "sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz", + "integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==", "cpu": [ "x64" ], @@ -1065,9 +1433,9 @@ ] }, "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.6.tgz", - "integrity": "sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz", + "integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==", "cpu": [ "x64" ], @@ -1081,9 +1449,9 @@ ] }, "node_modules/@rspack/binding-wasm32-wasi": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.6.tgz", - "integrity": "sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz", + "integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==", "cpu": [ "wasm32" ], @@ -1094,9 +1462,9 @@ } }, "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.6.tgz", - "integrity": "sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz", + "integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==", "cpu": [ "arm64" ], @@ -1107,9 +1475,9 @@ ] }, "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.6.tgz", - "integrity": "sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz", + "integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==", "cpu": [ "ia32" ], @@ -1120,9 +1488,9 @@ ] }, "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.6.tgz", - "integrity": "sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz", + "integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==", "cpu": [ "x64" ], @@ -1133,13 +1501,13 @@ ] }, "node_modules/@rspack/core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.6.tgz", - "integrity": "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==", + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz", + "integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==", "license": "MIT", "dependencies": { "@module-federation/runtime-tools": "0.22.0", - "@rspack/binding": "1.7.6", + "@rspack/binding": "1.7.11", "@rspack/lite-tapable": "1.1.0" }, "engines": { @@ -1179,10 +1547,223 @@ } } }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "license": "MIT", "optional": true, "dependencies": { @@ -1190,9 +1771,9 @@ } }, "node_modules/@types/dom-mediacapture-transform": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", - "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz", + "integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==", "license": "MIT", "dependencies": { "@types/dom-webcodecs": "*" @@ -1237,12 +1818,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@types/react": { @@ -1424,9 +2005,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1492,6 +2073,12 @@ "ajv": "^8.8.2" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -1505,9 +2092,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.33", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", - "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz", + "integrity": "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -1517,9 +2104,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "funding": [ { "type": "opencollective", @@ -1536,10 +2123,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -1555,10 +2142,31 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "funding": [ { "type": "opencollective", @@ -1590,6 +2198,38 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1640,9 +2280,9 @@ } }, "node_modules/css-loader/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1670,6 +2310,23 @@ "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -1679,6 +2336,16 @@ "node": ">=8" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -1692,15 +2359,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.364", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", - "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.22.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", - "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -1710,6 +2377,27 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -1720,15 +2408,15 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -1738,32 +2426,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -1870,9 +2558,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", @@ -1891,6 +2579,15 @@ "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", "license": "Unlicense" }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -1961,6 +2658,28 @@ "postcss": "^8.1.0" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -2020,6 +2739,46 @@ "node": ">= 10.13.0" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -2032,6 +2791,27 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kiwi-schema": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/kiwi-schema/-/kiwi-schema-0.5.0.tgz", + "integrity": "sha512-X+FpfU0yTEtc6aTHS7VwbOpvQwRt70+pXXWRI5fd6CvWhe7pSVC854TVo4Zo0x5/wwcWj+/9KUlXpdcP0dY9AA==", + "license": "MIT", + "bin": { + "kiwic": "cli.js" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -2041,6 +2821,12 @@ "node": ">=6" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -2060,22 +2846,28 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "yallist": "^3.0.2" } }, "node_modules/mediabunny": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.45.0.tgz", - "integrity": "sha512-oK3sMMYbucoF6LUX62L/2M9d+p9ve6KDQgL87kNfhsB0/XmTe9iRLUcgQgg9Gpgvi8Sb96zYfOUL6i17y0bdNg==", + "version": "1.50.8", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.50.8.tgz", + "integrity": "sha512-LgykLyQzhdpo0V2yw3UXmOpj+b4JAGdpHBwsPE6kjSt8Za0d1VllD+FV7EGHBcdV4+oHUAo+yrqbVAWxNSDCPQ==", "license": "MPL-2.0", "workspaces": [ ".", @@ -2144,10 +2936,16 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "license": "MIT" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -2168,10 +2966,20 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "license": "MIT", "engines": { "node": ">=18" @@ -2221,6 +3029,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2230,6 +3068,15 @@ "node": ">=8" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2237,9 +3084,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -2256,7 +3103,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2324,9 +3171,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -2380,24 +3227,24 @@ } }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.8" } }, "node_modules/react-refresh": { @@ -2435,9 +3282,9 @@ } }, "node_modules/remotion": { - "version": "4.0.469", - "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.469.tgz", - "integrity": "sha512-wD8LK3bFfcv44gnj8KLS1R1iGI+n0XGtPvdIiIaGfE+CTX3p2FJYKw2Vc5OfyfdVSACLn3r654XW9VteA9cTIQ==", + "version": "4.0.499", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.499.tgz", + "integrity": "sha512-QX5B1XDFBQpr8mYUBqhgCrH0cQJmqfI7zA46k9CsZhri/H9quXObYQBgeNDcxUhoulVvOOjVQqp5d0RAwB94HA==", "license": "SEE LICENSE IN LICENSE.md", "peerDependencies": { "react": ">=16.8.0", @@ -2453,6 +3300,15 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2493,6 +3349,24 @@ "node": ">=10" } }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2526,6 +3400,16 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -2613,6 +3497,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", @@ -2627,9 +3517,9 @@ } }, "node_modules/terser": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", - "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -2738,7 +3628,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2749,9 +3639,9 @@ } }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -2791,12 +3681,11 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "engines": { @@ -2858,9 +3747,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", - "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -2893,9 +3782,9 @@ } }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -2914,15 +3803,15 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/design/video/elsa-readme-video/package.json b/design/video/elsa-readme-video/package.json index 4962c9d07..97c9c7a70 100644 --- a/design/video/elsa-readme-video/package.json +++ b/design/video/elsa-readme-video/package.json @@ -9,10 +9,10 @@ "still": "remotion still ElsaReadme ../exports/elsa-workflows-readme-poster.png --frame=36" }, "dependencies": { - "@remotion/cli": "4.0.469", - "react": "19.2.6", - "react-dom": "19.2.6", - "remotion": "4.0.469" + "@remotion/cli": "4.0.499", + "react": "19.2.8", + "react-dom": "19.2.8", + "remotion": "4.0.499" }, "devDependencies": { "@types/react": "19.2.14", diff --git a/src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite.csproj b/src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite.csproj index 37d593bbc..a2448ec9e 100644 --- a/src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite.csproj +++ b/src/modules/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite/Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite.csproj @@ -13,6 +13,7 @@ + diff --git a/src/modules/Elsa.Persistence.EFCore.Sqlite/Elsa.Persistence.EFCore.Sqlite.csproj b/src/modules/Elsa.Persistence.EFCore.Sqlite/Elsa.Persistence.EFCore.Sqlite.csproj index 6ae2259c6..faf9d92e7 100644 --- a/src/modules/Elsa.Persistence.EFCore.Sqlite/Elsa.Persistence.EFCore.Sqlite.csproj +++ b/src/modules/Elsa.Persistence.EFCore.Sqlite/Elsa.Persistence.EFCore.Sqlite.csproj @@ -11,10 +11,11 @@ + - \ No newline at end of file + diff --git a/src/modules/Elsa.Persistence.VNext.Sqlite/Elsa.Persistence.VNext.Sqlite.csproj b/src/modules/Elsa.Persistence.VNext.Sqlite/Elsa.Persistence.VNext.Sqlite.csproj index a2790c882..129d9f8fd 100644 --- a/src/modules/Elsa.Persistence.VNext.Sqlite/Elsa.Persistence.VNext.Sqlite.csproj +++ b/src/modules/Elsa.Persistence.VNext.Sqlite/Elsa.Persistence.VNext.Sqlite.csproj @@ -9,6 +9,7 @@ + diff --git a/test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj b/test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj index f1c81a4f0..17e0b1bca 100644 --- a/test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj +++ b/test/integration/Elsa.AI.IntegrationTests/Elsa.AI.IntegrationTests.csproj @@ -6,6 +6,7 @@ + diff --git a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj index 54945fab7..52487bd20 100644 --- a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj +++ b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj @@ -7,6 +7,7 @@ + diff --git a/test/unit/Elsa.Persistence.VNext.UnitTests/Elsa.Persistence.VNext.UnitTests.csproj b/test/unit/Elsa.Persistence.VNext.UnitTests/Elsa.Persistence.VNext.UnitTests.csproj index aec7f67f4..475797a06 100644 --- a/test/unit/Elsa.Persistence.VNext.UnitTests/Elsa.Persistence.VNext.UnitTests.csproj +++ b/test/unit/Elsa.Persistence.VNext.UnitTests/Elsa.Persistence.VNext.UnitTests.csproj @@ -7,6 +7,7 @@ + From 1000d29feb49fd399376ba8f37afba036ce58470 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jul 2026 00:08:31 +0200 Subject: [PATCH 31/33] docs: refresh roadmap --- ROADMAP.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 297dcc64b..887789cfa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Elsa Roadmap -Last refreshed: 2026-07-22 +Last refreshed: 2026-07-29 This roadmap is a product direction document, not a fixed release calendar. Elsa is developed through a mix of core maintainer work, customer-funded work, and community contributions, so sequencing can change when real-world demand changes. The intent is stable: make Elsa the most productive, dependable, and extensible workflow platform for the .NET ecosystem. @@ -58,6 +58,7 @@ Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productizatio - [~] Workflow organization with labels/categories/folders - [~] Workflow progress/timeline surface - [x] Studio OIDC and identity modules +- [~] External-authentication broker, Studio SSO connection management, and configurable login themes (merged to `release/3.8.0`; release finalization pending) - [~] Studio diagnostics pages for structured logs, console logs, and OpenTelemetry - [~] Studio alterations module - [ ] Designer reliability/regression hardening @@ -112,6 +113,7 @@ These are already present in the codebase and should be treated as foundations f - Elsa Studio is already a modular Blazor product shell with workflow authoring, instance browsing, designer modules, diagnostics, authentication, localization, branding, custom elements, and early React wrapper work in [elsa-workflows/elsa-studio](https://github.com/elsa-workflows/elsa-studio). - Studio `3.7.0` shipped the modern authentication framework, Elsa Identity and OIDC modules, activity call-stack visualization, incident count badges, pending-instance filtering, and custom theme/DataPanel extensibility. - Studio `3.8.0-preview1` shipped the server logs module, console logs module, structured-log storage diagnostics, the OpenTelemetry diagnostics page from [elsa-studio#834](https://github.com/elsa-workflows/elsa-studio/pull/834), sequence and state-machine designer foundations, the secrets module, and the alterations designer. +- The unreleased `release/3.8.0` line now contains a protocol-neutral external-authentication broker with configuration- or database-backed OIDC connections, PKCE, secret handling, identity linking/JIT users, session management, persistence, and tests ([#7889](https://github.com/elsa-workflows/elsa-core/pull/7889)). Its Studio companions add Settings-based SSO connection management and a generic login-method UI ([elsa-studio#920](https://github.com/elsa-workflows/elsa-studio/pull/920)), plus configurable login themes ([elsa-studio#921](https://github.com/elsa-workflows/elsa-studio/pull/921)). This is a partially shipped foundation: the release line still needs final solution-level verification, release packaging, and operational documentation. - Elsa Extensions is an active modular integration repository with 70+ module projects in [elsa-workflows/elsa-extensions](https://github.com/elsa-workflows/elsa-extensions), targeting `net8.0`, `net9.0`, and `net10.0`. - Extensions already provide broad integration foundations: Connections, Secrets, Agents, OpenAPI, SQL/CSV/data tooling, messaging, schedulers, cloud storage, logging, webhooks, persistence providers, LDAP, and external system activities. - Extensions `3.7.0` adds package manifest metadata, infrastructure attributes, shell features for MassTransit/Quartz/Webhooks, Dapper and MongoDB activity execution-chain lookups, Dapper bookmark queue filtering, Kafka multitenancy/schema-trigger work, Quartz lifecycle/job cleanup fixes, and other operational hardening. @@ -183,7 +185,7 @@ High-value items: - Resolve the MassTransit strategy after the v9 licensing change. [discussion #6583](https://github.com/elsa-workflows/elsa-core/discussions/6583) raises a practical ecosystem risk; Elsa should either provide a clean split or reduce dependency weight through a smaller messaging abstraction. - Clarify Azure Functions and worker-service hosting patterns. [discussion #4707](https://github.com/elsa-workflows/elsa-core/discussions/4707) and [discussion #7420](https://github.com/elsa-workflows/elsa-core/discussions/7420) show demand for non-traditional hosts, Windows services, and serverless-adjacent deployments. - Add data movement and streaming workflow primitives. [#4809](https://github.com/elsa-workflows/elsa-core/issues/4809) frames this as datasets, linked services, transforms, and stream-oriented processing inspired by Azure Data Factory and stream analytics. -- Treat BPMN as interoperability first, not a wholesale product pivot. [#39](https://github.com/elsa-workflows/elsa-core/issues/39) has strong interest, but the pragmatic first slice is import/export or a constrained BPMN compatibility layer, not full BPMN engine parity. +- Treat BPMN as a strategic Elsa 4.0 candidate. A maintainer now says BPMN support is coming to Elsa 4.0 and has shared an early alpha screenshot ([#39 comment](https://github.com/elsa-workflows/elsa-core/issues/39#issuecomment-5085835900)). No public implementation branch or PR is available yet, so scope, interoperability commitments, and delivery sequencing remain roadmap work rather than a shipped promise. Recommended success measures: @@ -216,7 +218,7 @@ Recommended success measures: High-value items: -- Publish canonical OIDC recipes for Blazor Server, WASM, separate server/studio, all-in-one hosts, and reverse-proxy sub-path deployments. Studio `3.7.0` shipped the modern authentication modules, [#7181](https://github.com/elsa-workflows/elsa-core/issues/7181) shows Core-side implementation and documentation demand, and [elsa-studio#809](https://github.com/elsa-workflows/elsa-studio/pull/809) shows sub-path redirect URI handling is still being hardened. +- Productize the external-authentication and SSO foundation for Blazor Server, WASM, separate server/studio, all-in-one hosts, and reverse-proxy sub-path deployments. The unreleased `release/3.8.0` work adds an Elsa-owned extensible broker and OIDC adapter in Core ([#7889](https://github.com/elsa-workflows/elsa-core/pull/7889)), Settings-based SSO administration and generic login composition in Studio ([elsa-studio#920](https://github.com/elsa-workflows/elsa-studio/pull/920)), and configurable login themes ([elsa-studio#921](https://github.com/elsa-workflows/elsa-studio/pull/921)). Finish solution-level verification, operational recipes, migration/release guidance, and broad provider hardening without conflating upstream identity claims with Elsa authorization. - Provide a production security guide: API keys, JWT/OIDC, default admin bootstrap, scripting trust levels, C# expression risks, Docker demo boundaries, secret masking, tenant isolation, and permission design. - Expand authorization coverage tests around workflow instances, runtime admin, diagnostics, labels, tenants, and HTTP endpoint activities. - Add Studio governance controls: tenant/role-based activity visibility, granular permission-aware menus/routes, feature-gated modules, and clear behavior for hidden activities in existing workflow definitions. [elsa-studio#584](https://github.com/elsa-workflows/elsa-studio/issues/584) captures the authoring side of this enterprise need, and new issue [elsa-studio#908](https://github.com/elsa-workflows/elsa-studio/issues/908) sharpens the need for the Studio UI to honor granular permissions consistently. @@ -271,7 +273,7 @@ Longer term: 1. Native workflow-aware background execution and actor-runtime abstraction. 2. Data pipeline/stream processing primitives. -3. BPMN interoperability. +3. BPMN for Elsa 4.0, with public scope and implementation evidence still to be established. 4. AI-assisted authoring and workflow MCP tools. ## Maintainership Recommendations From ec9acd4f3f51b3b73584a4917a51472582beaa4a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jul 2026 02:47:44 +0200 Subject: [PATCH 32/33] Fix tenant service mutation race (#7898) Serialize tenant lifecycle mutations and keep synchronization available through shutdown. Closes #7771. --- .../Implementations/DefaultTenantService.cs | 42 +++-- .../Multitenancy/DefaultTenantServiceTests.cs | 146 ++++++++++++++++++ 2 files changed, 178 insertions(+), 10 deletions(-) diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs index 5dc7f0c05..95a336aa9 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs @@ -7,14 +7,13 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop { private readonly AsyncServiceScope _serviceScope = scopeFactory.CreateAsyncScope(); private readonly SemaphoreSlim _initializationLock = new(1, 1); - private readonly SemaphoreSlim _refreshLock = new(1, 1); + private readonly SemaphoreSlim _tenantMutationLock = new(1, 1); private IDictionary? _tenantsDictionary; private IDictionary? _tenantScopesDictionary; public async ValueTask DisposeAsync() { await _serviceScope.DisposeAsync(); - _initializationLock.Dispose(); } public async Task FindAsync(string id, CancellationToken cancellationToken = default) @@ -60,22 +59,33 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop public async Task DeactivateTenantsAsync(CancellationToken cancellationToken = default) { - var dictionary = await GetTenantsDictionaryAsync(cancellationToken); - var tenants = dictionary.Values.ToArray(); + var dictionary = await GetTenantsDictionaryForMutationAsync(cancellationToken); + await _tenantMutationLock.WaitAsync(cancellationToken); - foreach (var tenant in tenants) - await UnregisterTenantAsync(tenant, false, cancellationToken); + try + { + var tenants = dictionary.Values.ToArray(); + + foreach (var tenant in tenants) + { + await UnregisterTenantAsync(tenant, false, cancellationToken); + } + } + finally + { + _tenantMutationLock.Release(); + } } public async Task RefreshAsync(CancellationToken cancellationToken = default) { - await _refreshLock.WaitAsync(cancellationToken); + var currentTenants = await GetTenantsDictionaryForMutationAsync(cancellationToken); + await _tenantMutationLock.WaitAsync(cancellationToken); try { await using var scope = scopeFactory.CreateAsyncScope(); var tenantsProvider = scope.ServiceProvider.GetRequiredService(); - var currentTenants = await GetTenantsDictionaryAsync(cancellationToken); var currentTenantIds = currentTenants.Keys; var tenantsFromProvider = (await tenantsProvider.ListAsync(cancellationToken)).ToList(); var newTenants = tenantsFromProvider.Count == 0 @@ -99,10 +109,22 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop } finally { - _refreshLock.Release(); + _tenantMutationLock.Release(); } } + private async Task> GetTenantsDictionaryForMutationAsync(CancellationToken cancellationToken) + { + var dictionary = await GetTenantsDictionaryAsync(cancellationToken); + + // The dictionary is published before initialization completes so lifecycle event handlers can read it. + // Wait for any concurrent initializer before allowing a mutation to proceed. + await _initializationLock.WaitAsync(cancellationToken); + _initializationLock.Release(); + + return dictionary; + } + private async Task> GetTenantsDictionaryAsync(CancellationToken cancellationToken) { if (_tenantsDictionary == null) @@ -157,4 +179,4 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop } } } -} \ No newline at end of file +} diff --git a/test/unit/Elsa.Common.UnitTests/Multitenancy/DefaultTenantServiceTests.cs b/test/unit/Elsa.Common.UnitTests/Multitenancy/DefaultTenantServiceTests.cs index 89685a0d5..13566e714 100644 --- a/test/unit/Elsa.Common.UnitTests/Multitenancy/DefaultTenantServiceTests.cs +++ b/test/unit/Elsa.Common.UnitTests/Multitenancy/DefaultTenantServiceTests.cs @@ -119,6 +119,147 @@ public class DefaultTenantServiceTests } } + [Fact] + public async Task DeactivateTenantsAsync_WhenRefreshIsInProgress_WaitsForRefreshBeforeDeactivating() + { + var tenant = new Tenant { Id = "tenant-1", Name = "Tenant 1" }; + var timeout = TimeSpan.FromSeconds(5); + var refreshStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var completeRefresh = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); + var listRequests = 0; + var tenantsProvider = Substitute.For(); + tenantsProvider.ListAsync(Arg.Any()).Returns(async _ => + { + if (Interlocked.Increment(ref listRequests) == 1) + { + return new[] { tenant }; + } + + refreshStarted.TrySetResult(); + return await completeRefresh.Task; + }); + + var (tenantService, serviceProvider) = await CreateTenantServiceAsync(tenantsProvider); + Task? refreshTask = null; + Task? deactivateTask = null; + + try + { + await tenantService.ListAsync(); + + refreshTask = tenantService.RefreshAsync(); + await refreshStarted.Task.WaitAsync(timeout); + + deactivateTask = tenantService.DeactivateTenantsAsync(); + + Assert.False(deactivateTask.IsCompleted); + + completeRefresh.SetResult([tenant]); + await refreshTask.WaitAsync(timeout); + await deactivateTask.WaitAsync(timeout); + + Assert.Empty(await tenantService.ListAsync()); + } + finally + { + completeRefresh.TrySetResult([tenant]); + + if (refreshTask != null) + { + await refreshTask.WaitAsync(timeout); + } + + if (deactivateTask != null) + { + await deactivateTask.WaitAsync(timeout); + } + + if (tenantService is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + await serviceProvider.DisposeAsync(); + } + } + + [Fact] + public async Task DeactivateTenantsAsync_WhenInitializationIsInProgress_WaitsForInitializationBeforeDeactivating() + { + var tenant = new Tenant { Id = "tenant-1", Name = "Tenant 1" }; + var timeout = TimeSpan.FromSeconds(5); + var initializationStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var completeInitialization = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); + var tenantsProvider = Substitute.For(); + tenantsProvider.ListAsync(Arg.Any()).Returns(async _ => + { + initializationStarted.TrySetResult(); + return await completeInitialization.Task; + }); + + var (tenantService, serviceProvider) = await CreateTenantServiceAsync(tenantsProvider); + Task>? initializationTask = null; + Task? deactivateTask = null; + + try + { + initializationTask = tenantService.ListAsync(); + await initializationStarted.Task.WaitAsync(timeout); + + deactivateTask = tenantService.DeactivateTenantsAsync(); + + Assert.False(deactivateTask.IsCompleted); + + completeInitialization.SetResult([tenant]); + await initializationTask.WaitAsync(timeout); + await deactivateTask.WaitAsync(timeout); + + Assert.Empty(await tenantService.ListAsync()); + } + finally + { + completeInitialization.TrySetResult([tenant]); + + if (initializationTask != null) + { + await initializationTask.WaitAsync(timeout); + } + + if (deactivateTask != null) + { + await deactivateTask.WaitAsync(timeout); + } + + if (tenantService is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + await serviceProvider.DisposeAsync(); + } + } + + [Fact] + public async Task DeactivateTenantsAsync_AfterDisposeAsync_DoesNotUseDisposedSynchronizationPrimitives() + { + var tenant = new Tenant { Id = "tenant-1", Name = "Tenant 1" }; + var (tenantService, serviceProvider) = await CreateTenantServiceAsync([tenant]); + + try + { + await tenantService.ListAsync(); + await ((IAsyncDisposable)tenantService).DisposeAsync(); + + await tenantService.DeactivateTenantsAsync(); + + Assert.Empty(await tenantService.ListAsync()); + } + finally + { + await serviceProvider.DisposeAsync(); + } + } + private static Task<(ITenantService TenantService, ServiceProvider ServiceProvider)> CreateTenantServiceAsync(IEnumerable tenants, Func>? tenantsFactory = null) { var tenantList = tenants.ToList(); @@ -127,6 +268,11 @@ public class DefaultTenantServiceTests var tenantsProvider = Substitute.For(); tenantsProvider.ListAsync(Arg.Any()).Returns(_ => getTenants()); + return CreateTenantServiceAsync(tenantsProvider); + } + + private static Task<(ITenantService TenantService, ServiceProvider ServiceProvider)> CreateTenantServiceAsync(ITenantsProvider tenantsProvider) + { var services = new ServiceCollection(); services.AddSingleton(_ => tenantsProvider); services.AddSingleton(); From 56b16caa0e9cca7821d9e2fd766f8c0f2c3cbff5 Mon Sep 17 00:00:00 2001 From: Rostislav Statko Date: Thu, 30 Jul 2026 09:54:00 +0300 Subject: [PATCH 33/33] fix: restore request services after tenant middleware exceptions --- .../Middleware/TenantResolutionMiddleware.cs | 13 ++++-- .../Elsa.Tenants.UnitTests.csproj | 1 + .../TenantResolutionMiddlewareTests.cs | 43 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 test/unit/Elsa.Tenants.UnitTests/Middleware/TenantResolutionMiddlewareTests.cs diff --git a/src/modules/Elsa.Tenants.AspNetCore/Middleware/TenantResolutionMiddleware.cs b/src/modules/Elsa.Tenants.AspNetCore/Middleware/TenantResolutionMiddleware.cs index c4d35194f..b8babc087 100644 --- a/src/modules/Elsa.Tenants.AspNetCore/Middleware/TenantResolutionMiddleware.cs +++ b/src/modules/Elsa.Tenants.AspNetCore/Middleware/TenantResolutionMiddleware.cs @@ -37,7 +37,14 @@ public class TenantResolutionMiddleware(RequestDelegate next, ITenantScopeFactor await using var tenantScope = tenantScopeFactory.CreateScope(tenant); var originalServiceProvider = context.RequestServices; context.RequestServices = tenantScope.ServiceProvider; - await next(context); - context.RequestServices = originalServiceProvider; + + try + { + await next(context); + } + finally + { + context.RequestServices = originalServiceProvider; + } } -} \ No newline at end of file +} diff --git a/test/unit/Elsa.Tenants.UnitTests/Elsa.Tenants.UnitTests.csproj b/test/unit/Elsa.Tenants.UnitTests/Elsa.Tenants.UnitTests.csproj index 057369bbd..0a2af3fe0 100644 --- a/test/unit/Elsa.Tenants.UnitTests/Elsa.Tenants.UnitTests.csproj +++ b/test/unit/Elsa.Tenants.UnitTests/Elsa.Tenants.UnitTests.csproj @@ -7,6 +7,7 @@ + diff --git a/test/unit/Elsa.Tenants.UnitTests/Middleware/TenantResolutionMiddlewareTests.cs b/test/unit/Elsa.Tenants.UnitTests/Middleware/TenantResolutionMiddlewareTests.cs new file mode 100644 index 000000000..a1813367d --- /dev/null +++ b/test/unit/Elsa.Tenants.UnitTests/Middleware/TenantResolutionMiddlewareTests.cs @@ -0,0 +1,43 @@ +using Elsa.Common.Multitenancy; +using Elsa.Tenants.AspNetCore.Middleware; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace Elsa.Tenants.UnitTests.Middleware; + +public class TenantResolutionMiddlewareTests +{ + [Fact] + public async Task InvokeAsync_WhenNextThrows_RestoresOriginalRequestServices() + { + await using var rootProvider = new ServiceCollection() + .AddScoped(_ => new ScopedProbe()) + .BuildServiceProvider(); + await using var originalRequestScope = rootProvider.CreateAsyncScope(); + var originalRequestServices = originalRequestScope.ServiceProvider; + var context = new DefaultHttpContext { RequestServices = originalRequestServices }; + var expectedException = new InvalidOperationException("Downstream failure"); + var tenantScopeFactory = new DefaultTenantScopeFactory( + new DefaultTenantAccessor(), + rootProvider.GetRequiredService()); + var middleware = new TenantResolutionMiddleware( + _ => Task.FromException(expectedException), + tenantScopeFactory); + var tenantResolverPipelineInvoker = Substitute.For(); + tenantResolverPipelineInvoker + .InvokePipelineAsync(Arg.Any()) + .Returns(Task.FromResult(null)); + + var exception = await Assert.ThrowsAsync( + () => middleware.InvokeAsync(context, tenantResolverPipelineInvoker)); + + Assert.Same(expectedException, exception); + Assert.Same(originalRequestServices, context.RequestServices); + Assert.NotNull(context.RequestServices.GetRequiredService()); + } + + private sealed class ScopedProbe + { + } +}