| | | 1 | | using System.Security.Claims; |
| | | 2 | | using System.Text.Json; |
| | | 3 | | using Elsa.UserTasks.Models; |
| | | 4 | | |
| | | 5 | | namespace Elsa.UserTasks.Contracts; |
| | | 6 | | |
| | | 7 | | public interface IUserTaskIdentityResolver |
| | | 8 | | { |
| | | 9 | | ValueTask<UserTaskActor?> ResolveAsync(ClaimsPrincipal principal, CancellationToken cancellationToken = default); |
| | | 10 | | } |
| | | 11 | | |
| | | 12 | | public interface IUserTaskAccessPolicy |
| | | 13 | | { |
| | | 14 | | /// <summary> |
| | | 15 | | /// Builds the authorization predicate for a list request, or returns <c>null</c> when the actor may not |
| | | 16 | | /// list in the requested scope at all. Callers must treat <c>null</c> as a denial, never as "no filter". |
| | | 17 | | /// </summary> |
| | | 18 | | Task<UserTaskQueryScope?> CreateScopeAsync(UserTaskActor actor, UserTaskQueryScopeKind kind, CancellationToken cance |
| | | 19 | | Task<bool> AuthorizeAsync(UserTask task, UserTaskActor actor, UserTaskAccessOperation operation, CancellationToken c |
| | | 20 | | } |
| | | 21 | | |
| | | 22 | | public interface IUserTaskParticipantDirectory |
| | | 23 | | { |
| | | 24 | | Task<ParticipantSearchResult> SearchAsync(UserTaskParticipantQuery query, CancellationToken cancellationToken = defa |
| | | 25 | | Task<IReadOnlyCollection<ParticipantReference>> ResolveDisplayNamesAsync(IReadOnlyCollection<ParticipantReference> p |
| | | 26 | | Task<IReadOnlyCollection<ParticipantReference>> EnumerateGroupMembersAsync(ParticipantReference group, CancellationT |
| | | 27 | | } |
| | | 28 | | |
| | | 29 | | public interface IUserTaskFormProvider |
| | | 30 | | { |
| | | 31 | | string Name { get; } |
| | | 32 | | Task<ResolvedUserTaskForm?> ResolveAsync(UserTaskFormReference reference, CancellationToken cancellationToken = defa |
| | | 33 | | Task<UserTaskFormValidationResult> ValidateAndNormalizeAsync(ResolvedUserTaskForm form, string actionKey, JsonElemen |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | /// <summary> |
| | | 37 | | /// Signals that a task changed between being read and being saved. Every repository translates its own |
| | | 38 | | /// store-specific concurrency failure into this type, so callers never have to know which provider is |
| | | 39 | | /// installed to tell a lost optimistic-concurrency race from a genuine fault. |
| | | 40 | | /// </summary> |
| | | 41 | | public sealed class UserTaskRevisionConflictException(string taskId, int expectedRevision, Exception? innerException = n |
| | 6 | 42 | | : Exception($"User task '{taskId}' changed before revision {expectedRevision} could be saved.", innerException) |
| | | 43 | | { |
| | 9 | 44 | | public string TaskId { get; } = taskId; |
| | 9 | 45 | | public int ExpectedRevision { get; } = expectedRevision; |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | public interface IUserTaskRepository |
| | | 49 | | { |
| | | 50 | | Task<UserTask?> GetAsync(string tenantId, string taskId, CancellationToken cancellationToken = default); |
| | | 51 | | Task<UserTaskQueryResult> QueryAsync(UserTaskQuery query, CancellationToken cancellationToken = default); |
| | | 52 | | Task<UserTask?> FindByMaterializationKeyAsync(string tenantId, string key, CancellationToken cancellationToken = def |
| | | 53 | | Task<UserTask?> FindByBookmarkIdAsync(string tenantId, string bookmarkId, CancellationToken cancellationToken = defa |
| | | 54 | | |
| | | 55 | | /// <summary> |
| | | 56 | | /// Resolves the task that owns an invitation by the invitation's stored token hash. Lookup is |
| | | 57 | | /// deliberately hash-keyed and tenant-agnostic: an anonymous caller presents only a secret, and the |
| | | 58 | | /// provider must never be asked to trust a caller-supplied tenant or task identifier. |
| | | 59 | | /// </summary> |
| | | 60 | | Task<(UserTask Task, UserTaskInvitation Invitation)?> FindByInvitationTokenHashAsync(string tokenHash, CancellationT |
| | | 61 | | /// <summary>Throws <see cref="UserTaskRevisionConflictException"/> when the stored revision has moved on.</summary> |
| | | 62 | | Task SaveAsync(UserTask task, int expectedRevision, CancellationToken cancellationToken = default); |
| | | 63 | | Task AddProjectionAsync(UserTask task, CancellationToken cancellationToken = default); |
| | | 64 | | |
| | | 65 | | /// <summary> |
| | | 66 | | /// Appends one audit entry without touching the aggregate's revision. Audit is append-only and must not |
| | | 67 | | /// consume the optimistic-concurrency token: recording a read would otherwise invalidate the expected |
| | | 68 | | /// revision a client is already holding and turn its next command into a spurious conflict. |
| | | 69 | | /// </summary> |
| | | 70 | | Task AppendEventAsync(string tenantId, string taskId, UserTaskEvent @event, CancellationToken cancellationToken = de |
| | | 71 | | Task<bool> TryMutateAsync(string tenantId, string taskId, int expectedRevision, Func<UserTask, bool> mutation, Cance |
| | | 72 | | } |
| | | 73 | | |
| | | 74 | | public interface IUserTaskManager |
| | | 75 | | { |
| | | 76 | | Task<UserTaskProjectionResult> ProjectAsync(UserTaskMaterialization materialization, CancellationToken cancellationT |
| | | 77 | | /// <summary>Returns <c>null</c> when the actor may not list in the requested scope.</summary> |
| | | 78 | | Task<UserTaskQueryResultDto?> QueryAsync(UserTaskQuery query, UserTaskQueryScopeKind scope, UserTaskActor actor, Can |
| | | 79 | | Task<UserTaskDetail?> GetAsync(string tenantId, string taskId, UserTaskActor actor, CancellationToken cancellationTo |
| | | 80 | | Task<UserTaskCapabilities?> GetCapabilitiesAsync(string tenantId, string taskId, UserTaskActor actor, CancellationTo |
| | | 81 | | Task<UserTaskEventsResult?> GetEventsAsync(string tenantId, string taskId, string? cursor, int limit, UserTaskActor |
| | | 82 | | |
| | | 83 | | /// <summary> |
| | | 84 | | /// Discloses one masked form field after an explicit, audited request. Returns <c>null</c> when the task |
| | | 85 | | /// is invisible, the field is unknown, or the field was not marked revealable by its form provider. |
| | | 86 | | /// </summary> |
| | | 87 | | Task<JsonElement?> RevealFieldAsync(string tenantId, string taskId, string fieldKey, UserTaskActor actor, Cancellati |
| | | 88 | | Task<UserTaskOperationResult> ClaimAsync(string tenantId, string taskId, UserTaskMutationRequest request, UserTaskAc |
| | | 89 | | Task<UserTaskOperationResult> ReleaseAsync(string tenantId, string taskId, UserTaskMutationRequest request, UserTask |
| | | 90 | | Task<UserTaskOperationResult> AssignAsync(string tenantId, string taskId, UserTaskAssignRequest request, UserTaskAct |
| | | 91 | | Task<UserTaskOperationResult> UpdateSchedulingAsync(string tenantId, string taskId, UserTaskSchedulingUpdate request |
| | | 92 | | Task<UserTaskOperationResult> CompleteAsync(string tenantId, string taskId, UserTaskCompletionRequest request, UserT |
| | | 93 | | Task<UserTaskOperationResult> TimeoutAsync(string tenantId, string taskId, int expectedRevision, DateTimeOffset now, |
| | | 94 | | Task<UserTaskOperationResult> CancelAsync(string tenantId, string taskId, UserTaskCancelRequest request, UserTaskAct |
| | | 95 | | Task<UserTaskOperationResult> RetryResolutionAsync(string tenantId, string taskId, UserTaskMutationRequest request, |
| | | 96 | | } |
| | | 97 | | |
| | | 98 | | public interface IUserTaskProjectionService |
| | | 99 | | { |
| | | 100 | | Task ProjectCommittedBookmarksAsync(IReadOnlyCollection<UserTaskMaterialization> materializations, CancellationToken |
| | | 101 | | Task FinalizeBookmarkRemovalAsync(UserTaskBookmarkRemoval removal, CancellationToken cancellationToken = default); |
| | | 102 | | } |
| | | 103 | | |
| | | 104 | | public interface IUserTaskReconciler |
| | | 105 | | { |
| | | 106 | | Task<UserTaskReconciliationResult> ReconcileAsync(UserTaskReconciliationRequest request, CancellationToken cancellat |
| | | 107 | | } |
| | | 108 | | |
| | | 109 | | public interface IUserTaskDueService |
| | | 110 | | { |
| | | 111 | | Task<int> MarkOverdueAsync(string tenantId, DateTimeOffset? now = null, CancellationToken cancellationToken = defaul |
| | | 112 | | } |
| | | 113 | | |
| | | 114 | | public interface IUserTaskWorkflowResumer |
| | | 115 | | { |
| | | 116 | | Task ResumeAsync(UserTask task, UserTaskStimulus stimulus, CancellationToken cancellationToken = default); |
| | | 117 | | } |
| | | 118 | | |
| | | 119 | | public interface IUserTaskInvitationDispatcher |
| | | 120 | | { |
| | | 121 | | Task DispatchAsync(UserTaskInvitationDelivery delivery, CancellationToken cancellationToken = default); |
| | | 122 | | } |
| | | 123 | | |
| | | 124 | | public interface IUserTaskInvitationVerifier |
| | | 125 | | { |
| | | 126 | | Task<UserTaskInvitationVerificationResult> VerifyAsync(UserTaskInvitationChallenge challenge, CancellationToken canc |
| | | 127 | | } |
| | | 128 | | |
| | | 129 | | /// <summary> |
| | | 130 | | /// Issues and resolves revocable, task-scoped guest sessions. Implementations must store only a hash of the |
| | | 131 | | /// session credential and must invalidate a session as soon as its task closes. |
| | | 132 | | /// </summary> |
| | | 133 | | public interface IUserTaskGuestSessionIssuer |
| | | 134 | | { |
| | | 135 | | Task<GuestSessionResult> IssueAsync(UserTaskInvitation invitation, ParticipantReference subject, CancellationToken c |
| | | 136 | | |
| | | 137 | | /// <summary>Resolves a presented credential, or returns <c>null</c> for unknown, expired, or revoked sessions.</sum |
| | | 138 | | Task<UserTaskGuestSession?> ResolveAsync(string credential, CancellationToken cancellationToken = default); |
| | | 139 | | |
| | | 140 | | /// <summary>Revokes every session issued for a task. Called when the task reaches a terminal state.</summary> |
| | | 141 | | Task RevokeForTaskAsync(string tenantId, string taskId, CancellationToken cancellationToken = default); |
| | | 142 | | |
| | | 143 | | /// <summary> |
| | | 144 | | /// Revokes the sessions issued from one invitation. This is what makes a guest credential withdrawable: |
| | | 145 | | /// consuming an invitation is what creates the session, so revoking the invitation must kill it too. |
| | | 146 | | /// </summary> |
| | | 147 | | Task RevokeForInvitationAsync(string tenantId, string invitationId, CancellationToken cancellationToken = default); |
| | | 148 | | } |
| | | 149 | | |
| | | 150 | | /// <summary> |
| | | 151 | | /// Rate limiter for the anonymous invitation surface. The default implementation is a per-process sliding |
| | | 152 | | /// window; hosts running multiple replicas should replace it with a shared-store implementation. |
| | | 153 | | /// </summary> |
| | | 154 | | public interface IUserTaskInvitationRateLimiter |
| | | 155 | | { |
| | | 156 | | /// <summary>Returns <c>false</c> when the caller has exhausted its budget and must receive a 429.</summary> |
| | | 157 | | ValueTask<bool> TryAcquireAsync(string partitionKey, CancellationToken cancellationToken = default); |
| | | 158 | | } |
| | | 159 | | |
| | | 160 | | /// <summary> |
| | | 161 | | /// Holds invitation secrets between issuance and successful delivery. Entries are encrypted at rest through |
| | | 162 | | /// ASP.NET Core Data Protection and removed once delivery succeeds or the invitation expires. |
| | | 163 | | /// </summary> |
| | | 164 | | public interface IUserTaskInvitationOutbox |
| | | 165 | | { |
| | | 166 | | Task EnqueueAsync(UserTaskInvitationDelivery delivery, CancellationToken cancellationToken = default); |
| | | 167 | | Task<IReadOnlyCollection<UserTaskInvitationDelivery>> DequeueDueAsync(int maxCount, CancellationToken cancellationTo |
| | | 168 | | Task CompleteAsync(string deliveryId, CancellationToken cancellationToken = default); |
| | | 169 | | Task RescheduleAsync(string deliveryId, DateTimeOffset notBefore, CancellationToken cancellationToken = default); |
| | | 170 | | } |
| | | 171 | | |
| | | 172 | | public interface IUserTaskInvitationService |
| | | 173 | | { |
| | | 174 | | Task<UserTaskInvitationIssueResult?> IssueAsync(string tenantId, string taskId, UserTaskInvitationIssueRequest reque |
| | | 175 | | Task<IReadOnlyCollection<UserTaskInvitationSummary>?> ListAsync(string tenantId, string taskId, UserTaskActor actor, |
| | | 176 | | Task<bool> RevokeAsync(string tenantId, string taskId, string invitationId, int expectedRevision, UserTaskActor acto |
| | | 177 | | |
| | | 178 | | /// <summary> |
| | | 179 | | /// Describes the challenge an anonymous holder must answer. The descriptor is deliberately generic so a |
| | | 180 | | /// caller cannot distinguish a missing, expired, consumed, or revoked invitation from a valid one. |
| | | 181 | | /// </summary> |
| | | 182 | | Task<UserTaskInvitationChallengeDescriptor> DescribeAsync(string token, CancellationToken cancellationToken = defaul |
| | | 183 | | |
| | | 184 | | Task<UserTaskInvitationVerificationResultWithSession> VerifyAsync(UserTaskInvitationChallenge challenge, Cancellatio |
| | | 185 | | } |
| | | 186 | | |
| | | 187 | | public interface IUserTaskNotificationSink |
| | | 188 | | { |
| | | 189 | | Task PublishAsync(UserTaskLifecycleNotification notification, CancellationToken cancellationToken = default); |
| | | 190 | | } |