From f4ada4e87cfed96093446dc7d198c19aba10fa9e Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 17 Jun 2025 14:14:16 +0100 Subject: [PATCH 01/33] Boxcar evaluation --- .../AuthZenBoxcarRequestTests.cs | 342 +++++++ Rsk.AuthZen.Client.Test/AuthZenClientTests.cs | 941 +++++++++++++++++- Rsk.AuthZen.Client/AuthZenAction.cs | 10 + Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs | 153 +++ Rsk.AuthZen.Client/AuthZenClient.cs | 81 +- .../AuthZenEvaluationRequest.cs | 51 + Rsk.AuthZen.Client/AuthZenPayload.cs | 8 + .../AuthZenRequestFailureException.cs | 19 + Rsk.AuthZen.Client/AuthZenResource.cs | 11 + Rsk.AuthZen.Client/AuthZenResponse.cs | 9 + Rsk.AuthZen.Client/AuthZenSubject.cs | 11 + .../DTOs/AuthZenBoxcarRequestMessageDto.cs | 6 +- .../DTOs/AuthZenRequestMessageDto.cs | 8 +- Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs | 8 +- Rsk.AuthZen.Client/Decision.cs | 8 + Rsk.AuthZen.Client/IAuthZenClient.cs | 131 +-- Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs | 2 +- Rsk.AuthZen.sln.DotSettings.user | 8 +- 18 files changed, 1628 insertions(+), 179 deletions(-) create mode 100644 Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestTests.cs create mode 100644 Rsk.AuthZen.Client/AuthZenAction.cs create mode 100644 Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs create mode 100644 Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs create mode 100644 Rsk.AuthZen.Client/AuthZenPayload.cs create mode 100644 Rsk.AuthZen.Client/AuthZenRequestFailureException.cs create mode 100644 Rsk.AuthZen.Client/AuthZenResource.cs create mode 100644 Rsk.AuthZen.Client/AuthZenResponse.cs create mode 100644 Rsk.AuthZen.Client/AuthZenSubject.cs create mode 100644 Rsk.AuthZen.Client/Decision.cs diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestTests.cs b/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestTests.cs new file mode 100644 index 0000000..9164d3f --- /dev/null +++ b/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestTests.cs @@ -0,0 +1,342 @@ +using System.Collections.Generic; +using FluentAssertions; +using Xunit; + +namespace Rsk.AuthZen.Client.Test; + +public class AuthZenBoxcarRequestTests +{ + [Fact] + public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() + { + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "subject-id", + Type = "subject-type", + Properties = new Dictionary { { "key", "value" } } + } + }; + + + var request = new AuthZenBoxcarRequest + { + + }; + + var dto = request.ToDto(defaults); + + dto.Subject.Should().NotBeNull(); + dto.Subject.Id.Should().Be("subject-id"); + dto.Subject.Type.Should().Be("subject-type"); + dto.Subject.Properties.Keys.Should().Contain("key"); + dto.Subject.Properties["key"].Should().Be("value"); + } + + [Fact] + public void ToDto_WhenDefaultResourceIsSet_ShouldPopulateResource() + { + var defaults = new AuthZenBoxcarEvaluation() + { + Resource = new AuthZenResource + { + Id = "resource-id", + Type = "resource-type", + Properties = new Dictionary { { "key", "value" } } + } + }; + + var request = new AuthZenBoxcarRequest + { + + }; + + var dto = request.ToDto(defaults); + + dto.Resource.Should().NotBeNull(); + dto.Resource.Id.Should().Be("resource-id"); + dto.Resource.Type.Should().Be("resource-type"); + dto.Resource.Properties.Keys.Should().Contain("key"); + dto.Resource.Properties["key"].Should().Be("value"); + } + + [Fact] + public void ToDto_WhenDefaultActionIsSet_ShouldPopulateAction() + { + var defaults = new AuthZenBoxcarEvaluation() + { + Action = new AuthZenAction + { + Name = "action-name", + Properties = new Dictionary { { "key", "value" } } + } + }; + + var request = new AuthZenBoxcarRequest + { + + }; + + var dto = request.ToDto(defaults); + + dto.Action.Should().NotBeNull(); + dto.Action.Name.Should().Be("action-name"); + dto.Action.Properties.Keys.Should().Contain("key"); + dto.Action.Properties["key"].Should().Be("value"); + } + + [Fact] + public void ToDto_WhenDefaultContextIsSet_ShouldPopulateContext() + { + var defaults = new AuthZenBoxcarEvaluation() + { + Context = new Dictionary + { + { "contextKey", "contextValue" } + } + }; + + var request = new AuthZenBoxcarRequest + { + + }; + + var dto = request.ToDto(defaults); + + dto.Context.Should().NotBeNull(); + dto.Context.Keys.Should().Contain("contextKey"); + dto.Context["contextKey"].Should().Be("contextValue"); + } + + [Fact] + public void ToDto_WhenEvaluationsIsMissing_ShouldNotPopulateEvaluations() + { + var request = new AuthZenBoxcarRequest + { + Evaluations = null + }; + + var dto = request.ToDto(); + + dto.Evaluations.Should().BeNull(); + } + + [Fact] + public void ToDto_WhenEvaluationsIsEmpty_ShouldNotPopulateEvaluations() + { + var request = new AuthZenBoxcarRequest + { + Evaluations = new List() + }; + + var dto = request.ToDto(); + + dto.Evaluations.Should().BeNull(); + } + + [Fact] + public void ToDto_WhenEvaluationsIsSet_ShouldPopulateEachEvaluation() + { + var request = new AuthZenBoxcarRequest + { + Evaluations = new List + { + new () + { + Subject = new AuthZenSubject { Id = "eval-subject-id1", Type = "eval-subject-type1" }, + Resource = new AuthZenResource { Id = "eval-resource-id1", Type = "eval-resource-type1" }, + Action = new AuthZenAction { Name = "eval-action-name1", } + }, + + new () + { + Subject = new AuthZenSubject { Id = "eval-subject-id2", Type = "eval-subject-type2" }, + Resource = new AuthZenResource { Id = "eval-resource-id2", Type = "eval-resource-type2" }, + Action = new AuthZenAction { Name = "eval-action-name2", } + }, + + new () + { + Subject = new AuthZenSubject { Id = "eval-subject-id3", Type = "eval-subject-type3" }, + Resource = new AuthZenResource { Id = "eval-resource-id3", Type = "eval-resource-type3" }, + Action = new AuthZenAction { Name = "eval-action-name3", } + }, + } + }; + + var dto = request.ToDto(); + + dto.Evaluations.Should().NotBeNull(); + dto.Evaluations.Length.Should().Be(3); + + dto.Evaluations[0].Subject.Id.Should().Be("eval-subject-id1"); + dto.Evaluations[0].Subject.Type.Should().Be("eval-subject-type1"); + dto.Evaluations[0].Resource.Id.Should().Be("eval-resource-id1"); + dto.Evaluations[0].Resource.Type.Should().Be("eval-resource-type1"); + dto.Evaluations[0].Action.Name.Should().Be("eval-action-name1"); + + dto.Evaluations[1].Subject.Id.Should().Be("eval-subject-id2"); + dto.Evaluations[1].Subject.Type.Should().Be("eval-subject-type2"); + dto.Evaluations[1].Resource.Id.Should().Be("eval-resource-id2"); + dto.Evaluations[1].Resource.Type.Should().Be("eval-resource-type2"); + dto.Evaluations[1].Action.Name.Should().Be("eval-action-name2"); + + dto.Evaluations[2].Subject.Id.Should().Be("eval-subject-id3"); + dto.Evaluations[2].Subject.Type.Should().Be("eval-subject-type3"); + dto.Evaluations[2].Resource.Id.Should().Be("eval-resource-id3"); + dto.Evaluations[2].Resource.Type.Should().Be("eval-resource-type3"); + dto.Evaluations[2].Action.Name.Should().Be("eval-action-name3"); + } + + [Fact] + public void ToDto_WhenEvaluationSubjectIsSet_ShouldPopulateSubject() + { + var request = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new () + { + Subject = new AuthZenSubject + { + Id = "subject-id", + Type = "subject-type", + Properties = new Dictionary { { "key", "value" } } + } + } + } + }; + + var dto = request.ToDto(); + + dto.Evaluations[0].Subject.Should().NotBeNull(); + dto.Evaluations[0].Subject.Id.Should().Be("subject-id"); + dto.Evaluations[0].Subject.Type.Should().Be("subject-type"); + dto.Evaluations[0].Subject.Properties.Keys.Should().Contain("key"); + dto.Evaluations[0].Subject.Properties["key"].Should().Be("value"); + } + + [Fact] + public void ToDto_WhenEvaluationResourceIsSet_ShouldPopulateResource() + { + var request = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new () + { + Resource = new AuthZenResource + { + Id = "resource-id", + Type = "resource-type", + Properties = new Dictionary { { "key", "value" } } + } + } + } + + }; + + var dto = request.ToDto(); + + dto.Evaluations[0].Resource.Should().NotBeNull(); + dto.Evaluations[0].Resource.Id.Should().Be("resource-id"); + dto.Evaluations[0].Resource.Type.Should().Be("resource-type"); + dto.Evaluations[0].Resource.Properties.Keys.Should().Contain("key"); + dto.Evaluations[0].Resource.Properties["key"].Should().Be("value"); + } + + [Fact] + public void ToDto_WhenEvaluationActionIsSet_ShouldPopulateAction() + { + var request = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new () + { + Action = new AuthZenAction + { + Name = "action-name", + Properties = new Dictionary { { "key", "value" } } + } + } + } + }; + + var dto = request.ToDto(); + + dto.Evaluations[0].Action.Should().NotBeNull(); + dto.Evaluations[0].Action.Name.Should().Be("action-name"); + dto.Evaluations[0].Action.Properties.Keys.Should().Contain("key"); + dto.Evaluations[0].Action.Properties["key"].Should().Be("value"); + } + + [Fact] + public void ToDto_WhenEvaluationContextIsSet_ShouldPopulateContext() + { + var request = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new () + { + Context = new Dictionary + { + { "contextKey", "contextValue" } + } + } + } + }; + + var dto = request.ToDto(); + + dto.Evaluations[0].Context.Should().NotBeNull(); + dto.Evaluations[0].Context.Keys.Should().Contain("contextKey"); + dto.Evaluations[0].Context["contextKey"].Should().Be("contextValue"); + } + + [Theory] + [InlineData(BoxcarSemantics.DenyOnFirstDeny)] + [InlineData(BoxcarSemantics.PermitOnFirstPermit)] + [InlineData(BoxcarSemantics.ExecuteAll)] + public void ToDto_OptionsAreProvided_ShouldIncludeOptionsInRequestDto(BoxcarSemantics semantics) + { + var request = new AuthZenBoxcarRequest + { + Evaluations = new List + { + new () + { + Subject = new AuthZenSubject { Id = "eval-subject-id1", Type = "eval-subject-type1" }, + Resource = new AuthZenResource { Id = "eval-resource-id1", Type = "eval-resource-type1" }, + Action = new AuthZenAction { Name = "eval-action-name1", } + }, + + new () + { + Subject = new AuthZenSubject { Id = "eval-subject-id2", Type = "eval-subject-type2" }, + Resource = new AuthZenResource { Id = "eval-resource-id2", Type = "eval-resource-type2" }, + Action = new AuthZenAction { Name = "eval-action-name2", } + }, + + new () + { + Subject = new AuthZenSubject { Id = "eval-subject-id3", Type = "eval-subject-type3" }, + Resource = new AuthZenResource { Id = "eval-resource-id3", Type = "eval-resource-type3" }, + Action = new AuthZenAction { Name = "eval-action-name3", } + }, + } + }; + + var options = new AuthZenBoxcarOptions() + { + Semantics = semantics + }; + + var dto = request.ToDto(null, options); + + dto.Options.Should().NotBeNull(); + dto.Options.Should().BeEquivalentTo(options.ToDto()); + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs index 68cdaf5..4a3690f 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs @@ -24,11 +24,25 @@ public class AuthZenClientTests AuthZenClientOptions optionsValue; Mock> options; - private const string simpleEvaluationResponse = """ - { - "decision": true - } - """; + private const string simpleEvaluationResponse = + """ + { + "decision": true + } + """; + + private const string simpleBoxcarResponse = + """ + { + "evaluations": [ + { + "decision": true + } + ], + "correlationId": "12345" + } + """; + public AuthZenClientTests() { httpClientFactory = new Mock(); @@ -46,6 +60,65 @@ private AuthZenClient CreateSut() return new AuthZenClient(httpClientFactory?.Object, options?.Object); } + private async Task VerifyMissingRequestPartOmitsElement(AuthZenEvaluationRequest evaluationRequest, string expectedMissingElement) + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); + + var sut = CreateSut(); + + await sut.Evaluate(evaluationRequest); + + string sentContent = await requestSent.Content.ReadAsStringAsync(); + + var json = JsonDocument.Parse(sentContent); + + json.RootElement.TryGetProperty(expectedMissingElement, out JsonElement _).Should().BeFalse(); + } + + private void AdjustRequestSerialization(AuthZenRequestMessageDto request) + { + AdjustDeserializedDictionary(request.Subject?.Properties); + AdjustDeserializedDictionary(request.Resource?.Properties); + AdjustDeserializedDictionary(request.Action?.Properties); + AdjustDeserializedDictionary(request.Context); + } + + private void AdjustBoxcarRequestSerialization(AuthZenBoxcarRequestMessageDto request) + { + AdjustDeserializedDictionary(request.Subject?.Properties); + AdjustDeserializedDictionary(request.Resource?.Properties); + AdjustDeserializedDictionary(request.Action?.Properties); + AdjustDeserializedDictionary(request.Context); + } + + private void AdjustDeserializedDictionary(Dictionary properties) + { + if (properties == null) + { + return; + } + + foreach (string key in properties.Keys) + { + if (properties[key] is JsonElement e) + { + properties[key] = e.ValueKind switch + { + JsonValueKind.String => e.GetString(), + _ => e + }; + + } + } + } + [Fact] public void ctor_WhenPassedANullHttpClientFactory_ShouldThrowArgumentNullException() { @@ -494,8 +567,48 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndResponseContainsRequ authZenResponse.CorrelationId.Should().Be(expectedRequestId); } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPostToCorrectEndpoint() + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); + + var sut = CreateSut(); - private async Task VerifyMissingRequestPartOmitsElement(AuthZenEvaluationRequest evaluationRequest, string expectedMissingElement) + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation(); + + await sut.Evaluate(evaluationRequest, defaults); + + requestSent.Should().NotBeNull(); + requestSent.Method.Should().Be(HttpMethod.Post); + requestSent.RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.UriBase}/{AuthZenClient.BoxcarUri}"); + requestSent.Content.Headers.ContentType.MediaType.Should().Be("application/json"); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrelationId_ShouldAddRequestIdHeaderToRequest() { HttpRequestMessage requestSent = null; httpMessageHandler.Protected() @@ -503,41 +616,821 @@ private async Task VerifyMissingRequestPartOmitsElement(AuthZenEvaluationRequest .Callback((r, c) => requestSent = r) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(simpleEvaluationResponse) + Content = new StringContent(simpleBoxcarResponse) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + CorrelationId = Guid.NewGuid().ToString(), + Evaluations = new List + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation(); + + await sut.Evaluate(evaluationRequest, defaults); + + requestSent.Headers + .Should() + .ContainSingle(h => h.Key == "X-Request-ID" + && h.Value.Contains(evaluationRequest.CorrelationId)); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPostSerializedRequestCorrectly() + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) }); var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + CorrelationId = Guid.NewGuid().ToString(), + Evaluations = new List() + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; - await sut.Evaluate(evaluationRequest); + var defaults = new AuthZenBoxcarEvaluation(); + + await sut.Evaluate(evaluationRequest, defaults); string sentContent = await requestSent.Content.ReadAsStringAsync(); - var json = JsonDocument.Parse(sentContent); + AuthZenBoxcarRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); - json.RootElement.TryGetProperty(expectedMissingElement, out JsonElement _).Should().BeFalse(); + AdjustBoxcarRequestSerialization(deserializedRequest); + + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.ToDto()); } - - private void AdjustRequestSerialization(AuthZenRequestMessageDto request) + + [Theory] + [InlineData("true", Decision.Permit)] + [InlineData("false", Decision.Deny)] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldParseDecisionCorrectly(string jsonDecision, Decision expectedDecision) { - AdjustDeserializedDictionary(request.Subject?.Properties); - AdjustDeserializedDictionary(request.Resource?.Properties); - AdjustDeserializedDictionary(request.Action?.Properties); - AdjustDeserializedDictionary(request.Context); + string response = + $$""" + { + "evaluations": [ + { + "decision": {{jsonDecision}} + }, + { + "decision": {{jsonDecision}} + } + ] + } + """; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest() + { + Evaluations = new List() + { + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation(); + + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + + authZenResponse.Evaluations.Should().BeEquivalentTo(new List() + { + new () + { + Decision = expectedDecision, + Context = "" + }, + new () + { + Decision = expectedDecision, + Context = "" + }, + }); } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldExtractContextCorrectly() + { + string context = + """ + { + "sjdo": "sdfb", + "sdjfgn": 73, + "sakjdhvuob":{ + "sdfbvbui":true, + "hdfgouh": "iusdfvb" + } + } + """; - private void AdjustDeserializedDictionary(Dictionary properties) + string response = + $$""" + { + "evaluations" :[ + { + "decision": false, + "context": {{context}} + } + ] + } + """; + + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation(); + + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + + authZenResponse.Evaluations.Single().Context.Should().Be(context.Trim()); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRequestFails_ShouldThrowAuthZenRequestFailureException() { - foreach (string key in properties.Keys) + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest { - if (properties[key] is JsonElement e) + Evaluations = new List() { - properties[key] = e.ValueKind switch + new AuthZenBoxcarEvaluation() { - JsonValueKind.String => e.GetString(), - _ => e - }; + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation(); + + Func act = async () => await sut.Evaluate(evaluationRequest, defaults); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndResponseContainsRequestId_ShouldAddValueToAuthZenResponse() + { + string expectedRequestId = "khsdfibsduvb"; + string response = $$""" + { + "evaluations" :[ + { + "decision": false + } + ] + } + """; + + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response), + Headers = { { "X-Request-ID", expectedRequestId } } + }); + + var sut = CreateSut(); + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } } - } + }; + + var defaults = new AuthZenBoxcarEvaluation(); + + var authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + + authZenResponse.CorrelationId.Should().Be(expectedRequestId); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_ShouldPostToCorrectEndpoint() + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + await sut.Evaluate(evaluationRequest, defaults); + + requestSent.Should().NotBeNull(); + requestSent.Method.Should().Be(HttpMethod.Post); + requestSent.RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.UriBase}/{AuthZenClient.BoxcarUri}"); + requestSent.Content.Headers.ContentType.MediaType.Should().Be("application/json"); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWithCorrelationId_ShouldAddRequestIdHeaderToRequest() + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + CorrelationId = Guid.NewGuid().ToString(), + Evaluations = new List + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + await sut.Evaluate(evaluationRequest, defaults); + + requestSent.Headers + .Should() + .ContainSingle(h => h.Key == "X-Request-ID" + && h.Value.Contains(evaluationRequest.CorrelationId)); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_ShouldPostSerializedRequestCorrectly() + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + CorrelationId = Guid.NewGuid().ToString(), + Evaluations = new List() + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + await sut.Evaluate(evaluationRequest, defaults); + + string sentContent = await requestSent.Content.ReadAsStringAsync(); + + AuthZenBoxcarRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); + + AdjustBoxcarRequestSerialization(deserializedRequest); + + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.ToDto(defaults)); + } + + [Theory] + [InlineData("true", Decision.Permit)] + [InlineData("false", Decision.Deny)] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_ShouldParseDecisionCorrectly(string jsonDecision, Decision expectedDecision) + { + string response = + $$""" + { + "evaluations": [ + { + "decision": {{jsonDecision}} + }, + { + "decision": {{jsonDecision}} + } + ] + } + """; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest() + { + Evaluations = new List() + { + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + + authZenResponse.Evaluations.Should().BeEquivalentTo(new List() + { + new () + { + Decision = expectedDecision, + Context = "" + }, + new () + { + Decision = expectedDecision, + Context = "" + }, + }); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_ShouldExtractContextCorrectly() + { + string context = + """ + { + "sjdo": "sdfb", + "sdjfgn": 73, + "sakjdhvuob":{ + "sdfbvbui":true, + "hdfgouh": "iusdfvb" + } + } + """; + + string response = + $$""" + { + "evaluations" :[ + { + "decision": false, + "context": {{context}} + } + ] + } + """; + + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + + authZenResponse.Evaluations.Single().Context.Should().Be(context.Trim()); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAndRequestFails_ShouldThrowAuthZenRequestFailureException() + { + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + Func act = async () => await sut.Evaluate(evaluationRequest, defaults); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAndResponseContainsRequestId_ShouldAddValueToAuthZenResponse() + { + string expectedRequestId = "khsdfibsduvb"; + string response = $$""" + { + "evaluations" :[ + { + "decision": false + } + ] + } + """; + + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response), + Headers = { { "X-Request-ID", expectedRequestId } } + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List() + { + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + var authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + + authZenResponse.CorrelationId.Should().Be(expectedRequestId); + } + + [Theory] + [InlineData(BoxcarSemantics.DenyOnFirstDeny)] + [InlineData(BoxcarSemantics.ExecuteAll)] + [InlineData(BoxcarSemantics.PermitOnFirstPermit)] + public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarSemantics semantics) + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + CorrelationId = Guid.NewGuid().ToString(), + Evaluations = new List() + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + }; + + var defaults = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }; + + var boxcarOptions = new AuthZenBoxcarOptions + { + Semantics = semantics + }; + + await sut.Evaluate(evaluationRequest, defaults, boxcarOptions); + + string sentContent = await requestSent.Content.ReadAsStringAsync(); + + AuthZenBoxcarRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); + + AdjustBoxcarRequestSerialization(deserializedRequest); + + var expectation = evaluationRequest.ToDto(defaults, boxcarOptions); + deserializedRequest.Should().BeEquivalentTo(expectation); + } + + [Fact] + public async Task Evaluate_WhenBoxCarEvaluationsIsMissing_ShouldFallbackToSingleEvaluation() + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarRequest + { + Evaluations = new List() + }; + + var defaults = new AuthZenBoxcarEvaluation + { + Subject = new AuthZenSubject + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction + { + Name = "hjkldfgb" + } + }; + + await sut.Evaluate(evaluationRequest, defaults); + + requestSent.Should().NotBeNull(); + requestSent.Method.Should().Be(HttpMethod.Post); + requestSent.RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.UriBase}/{AuthZenClient.EvaluationUri}"); + requestSent.Content.Headers.ContentType.MediaType.Should().Be("application/json"); } } diff --git a/Rsk.AuthZen.Client/AuthZenAction.cs b/Rsk.AuthZen.Client/AuthZenAction.cs new file mode 100644 index 0000000..af6f565 --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenAction.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Rsk.AuthZen.Client +{ + public class AuthZenAction + { + public string Name { get; internal set; } + public Dictionary Properties { get; internal set; } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs b/Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs new file mode 100644 index 0000000..47d2fc3 --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using Rsk.AuthZen.Client.DTOs; + +namespace Rsk.AuthZen.Client +{ + public class AuthZenBoxcarRequest + { + public List Evaluations { get; internal set; } + + public AuthZenBoxcarEvaluation DefaultValues { get; internal set; } + + public AuthZenBoxcarOptions Options { get; internal set; } + + internal AuthZenBoxcarRequestMessageDto ToDto() + { + var dto = new AuthZenBoxcarRequestMessageDto(); + + if (DefaultValues?.Subject != null) + { + dto.Subject = new AuthZenSubjectDto + { + Id = DefaultValues.Subject.Id, + Type = DefaultValues.Subject.Type, + Properties = DefaultValues.Subject.Properties + }; + } + + if (DefaultValues?.Resource != null) + { + dto.Resource = new AuthZenResourceDto + { + Id = DefaultValues.Resource.Id, + Type = DefaultValues.Resource.Type, + Properties = DefaultValues.Resource.Properties + }; + } + + if (DefaultValues?.Action != null) + { + dto.Action = new AuthZenActionDto + { + Name = DefaultValues.Action.Name, + Properties = DefaultValues.Action.Properties + }; + } + + dto.Context = DefaultValues?.Context; + + if (Evaluations != null && Evaluations.Count > 0) + { + dto.Evaluations = new AuthZenRequestMessageDto[Evaluations.Count]; + for (int i = 0; i < Evaluations.Count; i++) + { + dto.Evaluations[i] = Evaluations[i].ToDto(); + } + } + + if (Options != null) + { + dto.Options = Options.ToDto(); + } + + return dto; + } + } + + public class AuthZenBoxcarResponse + { + public string CorrelationId { get; internal set; } + public List Evaluations { get; internal set; } + } + + public enum BoxcarSemantics + { + ExecuteAll, + DenyOnFirstDeny, + PermitOnFirstPermit + } + + // execute_all + // deny_on_first_deny + // permit_on_first_permit + public class AuthZenBoxcarOptions + { + public BoxcarSemantics Semantics { get; internal set; } + + internal AuthZenBoxcarOptionsDto ToDto() + { + return new AuthZenBoxcarOptionsDto + { + Evaluation_semantics = ConvertSemantics(Semantics) + }; + } + + private static string ConvertSemantics(BoxcarSemantics semantics) + { + return semantics switch + { + BoxcarSemantics.ExecuteAll => "execute_all", + BoxcarSemantics.DenyOnFirstDeny => "deny_on_first_deny", + BoxcarSemantics.PermitOnFirstPermit => "permit_on_first_permit", + _ => throw new ArgumentException($"Semantics value {semantics} is not supported ") + }; + } + } + + public class AuthZenBoxcarEvaluation + { + public AuthZenSubject Subject { get; internal set; } + public AuthZenResource Resource { get; internal set; } + public AuthZenAction Action { get; internal set; } + public Dictionary Context { get; internal set; } + + internal AuthZenRequestMessageDto ToDto() + { + var dto = new AuthZenRequestMessageDto(); + + if (Subject != null) + { + dto.Subject = new AuthZenSubjectDto + { + Id = Subject.Id, + Type = Subject.Type, + Properties = Subject.Properties + }; + } + + if (Resource != null) + { + dto.Resource = new AuthZenResourceDto + { + Id = Resource.Id, + Type = Resource.Type, + Properties = Resource.Properties + }; + } + + if (Action != null) + { + dto.Action = new AuthZenActionDto + { + Name = Action.Name, + Properties = Action.Properties + }; + } + + dto.Context = Context; + + return dto; + } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenClient.cs b/Rsk.AuthZen.Client/AuthZenClient.cs index 45aa526..554bdc5 100644 --- a/Rsk.AuthZen.Client/AuthZenClient.cs +++ b/Rsk.AuthZen.Client/AuthZenClient.cs @@ -11,7 +11,6 @@ namespace Rsk.AuthZen.Client { - public class AuthZenClientOptions { public string AuthorizationUrl { get; set; } @@ -25,7 +24,6 @@ public class AuthZenClient : IAuthZenClient internal const string BoxcarUri = "evaluations"; private const string RequestIdHeader = "X-Request-ID"; - private readonly HttpClient httpClient; private static JsonSerializerOptions serializerOptions = new JsonSerializerOptions @@ -75,37 +73,88 @@ public async Task Evaluate(AuthZenEvaluationRequest evaluationR authZenResponse.CorrelationId = requestIds.FirstOrDefault(); } - authZenResponse.Decision = responseDto.Decision ? Decision.Permit : Decision.Deny; authZenResponse.Context = responseDto.Context.ToString(); return authZenResponse; } - public Task> Evaluate(IEnumerable evaluationRequests, AuthZenEvaluationRequest requestDefaults) + public Task Evaluate(AuthZenBoxcarRequest request, AuthZenBoxcarEvaluation defaults) { - return Evaluate(evaluationRequests, requestDefaults, null); + return Evaluate(request, defaults, null); } - public Task> Evaluate(IEnumerable evaluationRequests, AuthZenEvaluationRequest requestDefaults, - AuthZenBoxcarOptions boxcarOptions) + public async Task Evaluate(AuthZenBoxcarRequest request, AuthZenBoxcarEvaluation defaults, AuthZenBoxcarOptions boxcarOptions) { - throw new NotImplementedException(); - } - } + if (IsMultiEvaluationsMissing(request)) + { + return await FallbackToSingleEvaluation(request, defaults); + } + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{BoxcarUri}", UriKind.Relative)); + + if (request.CorrelationId != null) + { + requestMessage.Headers.Add(RequestIdHeader, request.CorrelationId); + } + + string requestJson = JsonSerializer.Serialize(request.ToDto(defaults, boxcarOptions), serializerOptions); + + HttpContent content = new StringContent(requestJson, Encoding.UTF8, AuthZenContentType); + requestMessage.Content = content; + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) + { + throw new AuthZenRequestFailureException($"Evaluation request failed with status code: {responseMessage.StatusCode}"); + } + + string responseJson = await responseMessage.Content.ReadAsStringAsync(); + + AuthZenBoxcarResponseDto responseDto = JsonSerializer.Deserialize(responseJson, serializerOptions); + + var response = new AuthZenBoxcarResponse + { + Evaluations = responseDto.Evaluations.Select(e => new AuthZenResponse + { + Decision = e.Decision ? Decision.Permit : Decision.Deny, + Context = e.Context.ToString(), + }).ToList() + }; + + if (responseMessage.Headers.TryGetValues(RequestIdHeader, out IEnumerable requestIds)) + { + response.CorrelationId = requestIds.FirstOrDefault(); + } - public class AuthZenRequestFailureException : Exception - { - public AuthZenRequestFailureException() - { + return response; } - public AuthZenRequestFailureException(string message) : base(message) + private static bool IsMultiEvaluationsMissing(AuthZenBoxcarRequest request) { + return request.Evaluations == null || !request.Evaluations.Any(); } - public AuthZenRequestFailureException(string message, Exception inner) : base(message, inner) + private async Task FallbackToSingleEvaluation(AuthZenBoxcarRequest request, AuthZenBoxcarEvaluation defaults) { + var singleResponse = await Evaluate(new AuthZenEvaluationRequest() + { + Context = defaults.Context, + Subject = defaults.Subject, + Resource = defaults.Resource, + Action = defaults.Action, + CorrelationId = request.CorrelationId + }); + + return new AuthZenBoxcarResponse + { + Evaluations = new List + { + singleResponse + }, + CorrelationId = singleResponse.CorrelationId + }; } } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs b/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs new file mode 100644 index 0000000..ae635ac --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Rsk.AuthZen.Client.DTOs; + +namespace Rsk.AuthZen.Client +{ + public class AuthZenEvaluationRequest + { + public AuthZenSubject Subject { get; internal set; } + public AuthZenResource Resource { get; internal set; } + public AuthZenAction Action { get; internal set; } + public Dictionary Context { get; internal set; } + + internal AuthZenRequestMessageDto ToDto() + { + var dto = new AuthZenRequestMessageDto(); + + if (Subject != null) + { + dto.Subject = new AuthZenSubjectDto + { + Id = Subject.Id, + Type = Subject.Type, + Properties = Subject.Properties + }; + } + + if (Resource != null) + { + dto.Resource = new AuthZenResourceDto + { + Id = Resource.Id, + Type = Resource.Type, + Properties = Resource.Properties + }; + } + + if (Action != null) + { + dto.Action = new AuthZenActionDto + { + Name = Action.Name, + Properties = Action.Properties + }; + } + + dto.Context = Context; + + return dto; + } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenPayload.cs b/Rsk.AuthZen.Client/AuthZenPayload.cs new file mode 100644 index 0000000..39177c7 --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenPayload.cs @@ -0,0 +1,8 @@ +namespace Rsk.AuthZen.Client +{ + public class AuthZenPayload + { + public string CorrelationId { get; set; } + public T Payload { get; set; } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs b/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs new file mode 100644 index 0000000..b5672e1 --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Rsk.AuthZen.Client +{ + public class AuthZenRequestFailureException : Exception + { + public AuthZenRequestFailureException() + { + } + + public AuthZenRequestFailureException(string message) : base(message) + { + } + + public AuthZenRequestFailureException(string message, Exception inner) : base(message, inner) + { + } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenResource.cs b/Rsk.AuthZen.Client/AuthZenResource.cs new file mode 100644 index 0000000..b09ffec --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenResource.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Rsk.AuthZen.Client +{ + public class AuthZenResource + { + public string Id { get; internal set; } + public string Type { get; internal set; } + public Dictionary Properties { get; internal set; } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenResponse.cs b/Rsk.AuthZen.Client/AuthZenResponse.cs new file mode 100644 index 0000000..888d0ee --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenResponse.cs @@ -0,0 +1,9 @@ +namespace Rsk.AuthZen.Client +{ + public class AuthZenResponse + { + public Decision Decision { get; internal set; } + public string Context { get; internal set; } + public string CorrelationId { get; internal set; } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenSubject.cs b/Rsk.AuthZen.Client/AuthZenSubject.cs new file mode 100644 index 0000000..dc16fcc --- /dev/null +++ b/Rsk.AuthZen.Client/AuthZenSubject.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Rsk.AuthZen.Client +{ + public class AuthZenSubject + { + public string Id { get; internal set; } + public string Type { get; internal set; } + public Dictionary Properties { get; internal set; } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs b/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs index 8185f83..0de7591 100644 --- a/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs +++ b/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; - namespace Rsk.AuthZen.Client.DTOs { - public class AuthZenBoxcarRequestMessageDto : AuthZenRequestMessageDto + internal class AuthZenBoxcarRequestMessageDto : AuthZenRequestMessageDto { public AuthZenBoxcarOptionsDto Options { get; set; } public AuthZenRequestMessageDto[] Evaluations { get; set; } } - public class AuthZenBoxcarOptionsDto + internal class AuthZenBoxcarOptionsDto { public string Evaluation_semantics { get; set; } } diff --git a/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs b/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs index c172101..b70ead2 100644 --- a/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs +++ b/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs @@ -2,7 +2,7 @@ namespace Rsk.AuthZen.Client.DTOs { - public class AuthZenRequestMessageDto + internal class AuthZenRequestMessageDto { public AuthZenSubjectDto Subject { get; set; } public AuthZenResourceDto Resource { get; set; } @@ -10,21 +10,21 @@ public class AuthZenRequestMessageDto public Dictionary Context { get; set; } } - public class AuthZenSubjectDto + internal class AuthZenSubjectDto { public string Id { get; set; } public string Type { get; set; } public Dictionary Properties { get; set; } } - public class AuthZenResourceDto + internal class AuthZenResourceDto { public string Id { get; set; } public string Type { get; set; } public Dictionary Properties { get; set; } } - public class AuthZenActionDto + internal class AuthZenActionDto { public string Name { get; set; } public Dictionary Properties { get; set; } diff --git a/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs b/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs index efeb03e..b302339 100644 --- a/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs +++ b/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs @@ -1,10 +1,16 @@ +using System.Collections.Generic; using System.Text.Json; namespace Rsk.AuthZen.Client.DTOs { - public class AuthZenResponseDto + internal class AuthZenResponseDto { public bool Decision { get; set; } public JsonElement Context { get; set; } } + + internal class AuthZenBoxcarResponseDto + { + public List Evaluations { get; set; } + } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/Decision.cs b/Rsk.AuthZen.Client/Decision.cs new file mode 100644 index 0000000..bc33886 --- /dev/null +++ b/Rsk.AuthZen.Client/Decision.cs @@ -0,0 +1,8 @@ +namespace Rsk.AuthZen.Client +{ + public enum Decision + { + Permit, + Deny + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenClient.cs b/Rsk.AuthZen.Client/IAuthZenClient.cs index 43225c3..e9f5280 100644 --- a/Rsk.AuthZen.Client/IAuthZenClient.cs +++ b/Rsk.AuthZen.Client/IAuthZenClient.cs @@ -1,134 +1,19 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; -using Rsk.AuthZen.Client.DTOs; namespace Rsk.AuthZen.Client { public interface IAuthZenClient { - Task Evaluate(AuthZenEvaluationRequest evaluationRequest); - Task> Evaluate(IEnumerable evaluationRequests, AuthZenEvaluationRequest requestDefaults); - Task> Evaluate(IEnumerable evaluationRequests, AuthZenEvaluationRequest requestDefaults, AuthZenBoxcarOptions boxcarOptions); - } - - public enum Decision - { - Permit, - Deny - } - - public class AuthZenResponse - { - public Decision Decision { get; set; } - public string Context { get; set; } - public string CorrelationId { get; set; } - } - - public class AuthZenEvaluationRequest - { - internal AuthZenEvaluationRequest() - { - - } - - public AuthZenSubject Subject { get; internal set; } - public AuthZenResource Resource { get; internal set; } - public AuthZenAction Action { get; internal set; } - public Dictionary Context { get; internal set; } - public string CorrelationId { get; internal set; } - - internal AuthZenRequestMessageDto ToDto() - { - var dto = new AuthZenRequestMessageDto(); - - if (Subject != null) - { - dto.Subject = new AuthZenSubjectDto - { - Id = Subject.Id, - Type = Subject.Type, - Properties = Subject.Properties - }; - } - - if (Resource != null) - { - dto.Resource = new AuthZenResourceDto - { - Id = Resource.Id, - Type = Resource.Type, - Properties = Resource.Properties - }; - } - - if (Action != null) - { - dto.Action = new AuthZenActionDto - { - Name = Action.Name, - Properties = Action.Properties - }; - } - - dto.Context = Context; - - return dto; - } - } - - public class AuthZenSubject - { - public string Id { get; internal set; } - public string Type { get; internal set; } - public Dictionary Properties { get; internal set; } - } - - public class AuthZenResource - { - public string Id { get; internal set; } - public string Type { get; internal set; } - public Dictionary Properties { get; internal set; } - } - - public class AuthZenAction - { - public string Name { get; internal set; } - public Dictionary Properties { get; internal set; } - } - - public enum BoxcarSemantics - { - ExecuteAll, - DenyOnFirstDeny, - PermitOnFirstPermit - } - - // execute_all - // deny_on_first_deny - // permit_on_first_permit - public class AuthZenBoxcarOptions - { - public BoxcarSemantics Semantics { get; set; } - + Task Evaluate( + AuthZenPayload evaluationRequest); - public AuthZenBoxcarOptionsDto ToDto() - { - return new AuthZenBoxcarOptionsDto - { - Evaluation_semantics = ConvertSemantics(Semantics) - }; - } + Task Evaluate( + AuthZenPayload request); - private static string ConvertSemantics(BoxcarSemantics semantics) - { - return semantics switch - { - BoxcarSemantics.ExecuteAll => "execute_all", - BoxcarSemantics.DenyOnFirstDeny => "deny_on_first_deny", - BoxcarSemantics.PermitOnFirstPermit => "permit_on_first_permit", - _ => throw new ArgumentException($"Semantics value {semantics} is not supported ") - }; - } + // Task Evaluate( + // AuthZenBoxcarRequest request, + // AuthZenBoxcarEvaluation defaults, + // AuthZenBoxcarOptions boxcarOptions); } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs index bb4c04b..db6dcbd 100644 --- a/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs +++ b/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs @@ -10,7 +10,7 @@ public interface IAuthZenRequestBuilder IAuthZenPropertyBag SetContext(); IAuthZenRequestBuilder SetCorrelationId(string correlationId); - AuthZenEvaluationRequest Build(); + AuthZenPayload Build(); } public interface IAuthZenPropertyBag diff --git a/Rsk.AuthZen.sln.DotSettings.user b/Rsk.AuthZen.sln.DotSettings.user index 15c86ec..3b80a40 100644 --- a/Rsk.AuthZen.sln.DotSettings.user +++ b/Rsk.AuthZen.sln.DotSettings.user @@ -1,10 +1,6 @@  + ForceIncluded ForceIncluded <SessionState ContinuousTestingMode="0" IsActive="True" Name="AuthZenClientTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>xUnit::34403A6D-D7DF-45E5-8754-AF74DEB97B10::net8.0::Rsk.AuthZen.Client.Test.AuthZenClientTests</TestId> - <TestId>xUnit::34403A6D-D7DF-45E5-8754-AF74DEB97B10::net8.0::Rsk.AuthZen.Client.Test.AuthZenRequestBuilderTests</TestId> - <TestId>xUnit::34403A6D-D7DF-45E5-8754-AF74DEB97B10::net8.0::Rsk.AuthZen.Client.Test.AuthZenPropertyBagTests</TestId> - <TestId>xUnit::34403A6D-D7DF-45E5-8754-AF74DEB97B10::net8.0::Rsk.AuthZen.Client.Test.AuthZenBoxCarOptionsTests.ToDto_WhenCalled_ShouldTranslateSemanticsCorrectly</TestId> - </TestAncestor> + <Solution /> </SessionState> \ No newline at end of file From 9b618642157481ecc058ca1eb3134d22a14ffdcc Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 17 Jun 2025 15:19:03 +0100 Subject: [PATCH 02/33] Rework to request wrapper --- ...=> AuthZenBoxcarEvaluationRequestTests.cs} | 57 +- Rsk.AuthZen.Client.Test/AuthZenClientTests.cs | 937 ++++++++++-------- .../AuthZenRequestBuilderTests.cs | 76 +- ...t.cs => AuthZenBoxcarEvaluationRequest.cs} | 4 +- Rsk.AuthZen.Client/AuthZenClient.cs | 40 +- Rsk.AuthZen.Client/AuthZenPayload.cs | 4 +- Rsk.AuthZen.Client/AuthZenRequestBuilder.cs | 13 +- ...t.cs => AuthZenSingleEvaluationRequest.cs} | 2 +- Rsk.AuthZen.Client/IAuthZenClient.cs | 9 +- Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs | 2 +- 10 files changed, 601 insertions(+), 543 deletions(-) rename Rsk.AuthZen.Client.Test/{AuthZenBoxcarRequestTests.cs => AuthZenBoxcarEvaluationRequestTests.cs} (90%) rename Rsk.AuthZen.Client/{AuthZenBoxcarRequest.cs => AuthZenBoxcarEvaluationRequest.cs} (98%) rename Rsk.AuthZen.Client/{AuthZenEvaluationRequest.cs => AuthZenSingleEvaluationRequest.cs} (96%) diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestTests.cs b/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs similarity index 90% rename from Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestTests.cs rename to Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs index 9164d3f..6a03dcd 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs @@ -4,7 +4,7 @@ namespace Rsk.AuthZen.Client.Test; -public class AuthZenBoxcarRequestTests +public class AuthZenBoxcarEvaluationRequestTests { [Fact] public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() @@ -20,12 +20,12 @@ public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() }; - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { - + DefaultValues = defaults }; - var dto = request.ToDto(defaults); + var dto = request.ToDto(); dto.Subject.Should().NotBeNull(); dto.Subject.Id.Should().Be("subject-id"); @@ -47,12 +47,12 @@ public void ToDto_WhenDefaultResourceIsSet_ShouldPopulateResource() } }; - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { - + DefaultValues = defaults }; - var dto = request.ToDto(defaults); + var dto = request.ToDto(); dto.Resource.Should().NotBeNull(); dto.Resource.Id.Should().Be("resource-id"); @@ -73,12 +73,12 @@ public void ToDto_WhenDefaultActionIsSet_ShouldPopulateAction() } }; - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { - + DefaultValues = defaults }; - var dto = request.ToDto(defaults); + var dto = request.ToDto(); dto.Action.Should().NotBeNull(); dto.Action.Name.Should().Be("action-name"); @@ -97,12 +97,12 @@ public void ToDto_WhenDefaultContextIsSet_ShouldPopulateContext() } }; - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { - + DefaultValues = defaults }; - var dto = request.ToDto(defaults); + var dto = request.ToDto(); dto.Context.Should().NotBeNull(); dto.Context.Keys.Should().Contain("contextKey"); @@ -112,7 +112,7 @@ public void ToDto_WhenDefaultContextIsSet_ShouldPopulateContext() [Fact] public void ToDto_WhenEvaluationsIsMissing_ShouldNotPopulateEvaluations() { - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = null }; @@ -125,7 +125,7 @@ public void ToDto_WhenEvaluationsIsMissing_ShouldNotPopulateEvaluations() [Fact] public void ToDto_WhenEvaluationsIsEmpty_ShouldNotPopulateEvaluations() { - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = new List() }; @@ -138,7 +138,7 @@ public void ToDto_WhenEvaluationsIsEmpty_ShouldNotPopulateEvaluations() [Fact] public void ToDto_WhenEvaluationsIsSet_ShouldPopulateEachEvaluation() { - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = new List { @@ -192,7 +192,7 @@ public void ToDto_WhenEvaluationsIsSet_ShouldPopulateEachEvaluation() [Fact] public void ToDto_WhenEvaluationSubjectIsSet_ShouldPopulateSubject() { - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = new List() { @@ -220,7 +220,7 @@ public void ToDto_WhenEvaluationSubjectIsSet_ShouldPopulateSubject() [Fact] public void ToDto_WhenEvaluationResourceIsSet_ShouldPopulateResource() { - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = new List() { @@ -249,7 +249,7 @@ public void ToDto_WhenEvaluationResourceIsSet_ShouldPopulateResource() [Fact] public void ToDto_WhenEvaluationActionIsSet_ShouldPopulateAction() { - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = new List() { @@ -275,7 +275,7 @@ public void ToDto_WhenEvaluationActionIsSet_ShouldPopulateAction() [Fact] public void ToDto_WhenEvaluationContextIsSet_ShouldPopulateContext() { - var request = new AuthZenBoxcarRequest + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = new List() { @@ -302,7 +302,12 @@ public void ToDto_WhenEvaluationContextIsSet_ShouldPopulateContext() [InlineData(BoxcarSemantics.ExecuteAll)] public void ToDto_OptionsAreProvided_ShouldIncludeOptionsInRequestDto(BoxcarSemantics semantics) { - var request = new AuthZenBoxcarRequest + var options = new AuthZenBoxcarOptions() + { + Semantics = semantics + }; + + var request = new AuthZenBoxcarEvaluationRequest { Evaluations = new List { @@ -326,15 +331,11 @@ public void ToDto_OptionsAreProvided_ShouldIncludeOptionsInRequestDto(BoxcarSema Resource = new AuthZenResource { Id = "eval-resource-id3", Type = "eval-resource-type3" }, Action = new AuthZenAction { Name = "eval-action-name3", } }, - } - }; - - var options = new AuthZenBoxcarOptions() - { - Semantics = semantics + }, + Options = options }; - var dto = request.ToDto(null, options); + var dto = request.ToDto(); dto.Options.Should().NotBeNull(); dto.Options.Should().BeEquivalentTo(options.ToDto()); diff --git a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs index 4a3690f..3d843b8 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs @@ -60,7 +60,7 @@ private AuthZenClient CreateSut() return new AuthZenClient(httpClientFactory?.Object, options?.Object); } - private async Task VerifyMissingRequestPartOmitsElement(AuthZenEvaluationRequest evaluationRequest, string expectedMissingElement) + private async Task VerifyMissingRequestPartOmitsElement(AuthZenPayload singleEvaluationRequest, string expectedMissingElement) { HttpRequestMessage requestSent = null; httpMessageHandler.Protected() @@ -73,7 +73,7 @@ private async Task VerifyMissingRequestPartOmitsElement(AuthZenEvaluationRequest var sut = CreateSut(); - await sut.Evaluate(evaluationRequest); + await sut.Evaluate(singleEvaluationRequest); string sentContent = await requestSent.Content.ReadAsStringAsync(); @@ -182,12 +182,15 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostToCorrectEnd var sut = CreateSut(); - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } }; @@ -213,12 +216,15 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationWithCorrelationId_Shoul var sut = CreateSut(); - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + }, }, CorrelationId = "pioxjhdfvbghdsfohiv" }; @@ -243,42 +249,45 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRe var sut = CreateSut(); - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "dasfgthb", - Type = "aerfbqret", - Properties = new Dictionary + Subject = new AuthZenSubject { - { "dfgbfd", "sdfbf" }, - { "sdfbfdsb", "sdfbsdf" } - } - }, - Resource = new AuthZenResource - { - Id = "fdgn", - Type = "bfda", - Properties = new Dictionary + Id = "dasfgthb", + Type = "aerfbqret", + Properties = new Dictionary + { + { "dfgbfd", "sdfbf" }, + { "sdfbfdsb", "sdfbsdf" } + } + }, + Resource = new AuthZenResource { - { "dgfnsg", "bnfa" }, - { "bfea", "fba" } - } - }, - Action = new AuthZenAction - { - Name = "paioshd", - Properties = new Dictionary + Id = "fdgn", + Type = "bfda", + Properties = new Dictionary + { + { "dgfnsg", "bnfa" }, + { "bfea", "fba" } + } + }, + Action = new AuthZenAction { - { "bdfaa", "aefdba" }, - { "aedfb", "bfda" } - } - }, - Context = new Dictionary + Name = "paioshd", + Properties = new Dictionary + { + { "bdfaa", "aefdba" }, + { "aedfb", "bfda" } + } + }, + Context = new Dictionary { { "dabgn", "bfra" }, { "htearha", "erhaer" } } + } }; await sut.Evaluate(evaluationRequest); @@ -290,38 +299,41 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRe AdjustRequestSerialization(deserializedRequest); - deserializedRequest.Should().BeEquivalentTo(evaluationRequest.ToDto()); + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Payload.ToDto()); } [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoSubject_ShouldNotSerializeSubject() { - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Resource = new AuthZenResource + Payload = new AuthZenSingleEvaluationRequest() { - Id = "fdgn", - Type = "bfda", - Properties = new Dictionary + Resource = new AuthZenResource { - { "dgfnsg", "bnfa" }, - { "bfea", "fba" } - } - }, - Action = new AuthZenAction - { - Name = "paioshd", - Properties = new Dictionary + Id = "fdgn", + Type = "bfda", + Properties = new Dictionary + { + { "dgfnsg", "bnfa" }, + { "bfea", "fba" } + } + }, + Action = new AuthZenAction { - { "bdfaa", "aefdba" }, - { "aedfb", "bfda" } - } - }, - Context = new Dictionary + Name = "paioshd", + Properties = new Dictionary + { + { "bdfaa", "aefdba" }, + { "aedfb", "bfda" } + } + }, + Context = new Dictionary { { "dabgn", "bfra" }, { "htearha", "erhaer" } } + } }; await VerifyMissingRequestPartOmitsElement(evaluationRequest, "subject"); @@ -330,32 +342,35 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoSubject_ShouldNotS [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoResource_ShouldNotSerializeResource() { - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "fdgn", - Type = "bfda", - Properties = new Dictionary + Subject = new AuthZenSubject { - { "dgfnsg", "bnfa" }, - { "bfea", "fba" } - } - }, - Action = new AuthZenAction - { - Name = "paioshd", - Properties = new Dictionary + Id = "fdgn", + Type = "bfda", + Properties = new Dictionary + { + { "dgfnsg", "bnfa" }, + { "bfea", "fba" } + } + }, + Action = new AuthZenAction { - { "bdfaa", "aefdba" }, - { "aedfb", "bfda" } - } - }, - Context = new Dictionary + Name = "paioshd", + Properties = new Dictionary + { + { "bdfaa", "aefdba" }, + { "aedfb", "bfda" } + } + }, + Context = new Dictionary { { "dabgn", "bfra" }, { "htearha", "erhaer" } } + } }; await VerifyMissingRequestPartOmitsElement(evaluationRequest, "resource"); @@ -364,32 +379,36 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoResource_ShouldNot [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoAction_ShouldNotSerializeAction() { - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() + { - Id = "fdgn", - Type = "bfda", - Properties = new Dictionary + Subject = new AuthZenSubject { - { "dgfnsg", "bnfa" }, - { "bfea", "fba" } - } - }, - Resource = new AuthZenResource - { - Id = "asdefhb", - Type = "aehb", - Properties = new Dictionary + Id = "fdgn", + Type = "bfda", + Properties = new Dictionary + { + { "dgfnsg", "bnfa" }, + { "bfea", "fba" } + } + }, + Resource = new AuthZenResource { - { "bre", "asreh" }, - } - }, - Context = new Dictionary + Id = "asdefhb", + Type = "aehb", + Properties = new Dictionary + { + { "bre", "asreh" }, + } + }, + Context = new Dictionary { { "dabgn", "bfra" }, { "htearha", "erhaer" } } + } }; await VerifyMissingRequestPartOmitsElement(evaluationRequest, "action"); @@ -398,34 +417,37 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoAction_ShouldNotSe [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoContext_ShouldNotSerializeContext() { - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "fdgn", - Type = "bfda", - Properties = new Dictionary + Subject = new AuthZenSubject { - { "dgfnsg", "bnfa" }, - { "bfea", "fba" } - } - }, - Resource = new AuthZenResource - { - Id = "asdefhb", - Type = "aehb", - Properties = new Dictionary + Id = "fdgn", + Type = "bfda", + Properties = new Dictionary + { + { "dgfnsg", "bnfa" }, + { "bfea", "fba" } + } + }, + Resource = new AuthZenResource { - { "bre", "asreh" }, - } - }, - Action = new AuthZenAction - { - Name = "paioshd", - Properties = new Dictionary + Id = "asdefhb", + Type = "aehb", + Properties = new Dictionary + { + { "bre", "asreh" }, + } + }, + Action = new AuthZenAction { - { "dabgn", "bfra" }, - { "htearha", "erhaer" } + Name = "paioshd", + Properties = new Dictionary + { + { "dabgn", "bfra" }, + { "htearha", "erhaer" } + } } } }; @@ -452,12 +474,15 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldParseDecisionCor var sut = CreateSut(); - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } }; @@ -496,12 +521,15 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldExtractContextCo var sut = CreateSut(); - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } }; @@ -520,12 +548,15 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndRequestFails_ShouldT var sut = CreateSut(); - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } }; @@ -554,12 +585,15 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndResponseContainsRequ var sut = CreateSut(); - var evaluationRequest = new AuthZenEvaluationRequest + var evaluationRequest = new AuthZenPayload { - Subject = new AuthZenSubject + Payload = new AuthZenSingleEvaluationRequest() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } }; @@ -582,24 +616,27 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload { - Evaluations = new List + Payload = new AuthZenBoxcarEvaluationRequest() { - new() + Evaluations = new List { - Subject = new AuthZenSubject + new() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } - } + }, + DefaultValues = new AuthZenBoxcarEvaluation() } }; - - var defaults = new AuthZenBoxcarEvaluation(); - await sut.Evaluate(evaluationRequest, defaults); + + await sut.Evaluate(evaluationRequest); requestSent.Should().NotBeNull(); requestSent.Method.Should().Be(HttpMethod.Post); @@ -621,25 +658,27 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrel var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload { CorrelationId = Guid.NewGuid().ToString(), - Evaluations = new List + Payload = new AuthZenBoxcarEvaluationRequest() { - new() + Evaluations = new List { - Subject = new AuthZenSubject + new() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } - } - } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + }, }; - var defaults = new AuthZenBoxcarEvaluation(); - - await sut.Evaluate(evaluationRequest, defaults); + await sut.Evaluate(evaluationRequest); requestSent.Headers .Should() @@ -661,25 +700,27 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload { CorrelationId = Guid.NewGuid().ToString(), - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new() + Evaluations = new List() { - Subject = new AuthZenSubject + new() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } - } + }, + DefaultValues = new AuthZenBoxcarEvaluation() } }; - var defaults = new AuthZenBoxcarEvaluation(); - - await sut.Evaluate(evaluationRequest, defaults); + await sut.Evaluate(evaluationRequest); string sentContent = await requestSent.Content.ReadAsStringAsync(); @@ -688,7 +729,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos AdjustBoxcarRequestSerialization(deserializedRequest); - deserializedRequest.Should().BeEquivalentTo(evaluationRequest.ToDto()); + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Payload.ToDto()); } [Theory] @@ -718,24 +759,25 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPar var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest() + var evaluationRequest = new AuthZenPayload { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new AuthZenBoxcarEvaluation() + Evaluations = new List() { - Subject = new AuthZenSubject - { - Id = "dasfgthb", - Type = "aerfbqret" - } + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } } } }; - var defaults = new AuthZenBoxcarEvaluation(); - - AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest); authZenResponse.Evaluations.Should().BeEquivalentTo(new List() { @@ -788,24 +830,25 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldExt var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest { - new AuthZenBoxcarEvaluation() + Evaluations = new List() { - Subject = new AuthZenSubject - { - Id = "dasfgthb", - Type = "aerfbqret" - } + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } } } }; - var defaults = new AuthZenBoxcarEvaluation(); - - AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest); authZenResponse.Evaluations.Single().Context.Should().Be(context.Trim()); } @@ -820,24 +863,25 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRequest var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest { - new AuthZenBoxcarEvaluation() + Evaluations = new List() { - Subject = new AuthZenSubject + new AuthZenBoxcarEvaluation() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } } } }; - var defaults = new AuthZenBoxcarEvaluation(); - - Func act = async () => await sut.Evaluate(evaluationRequest, defaults); + Func act = async () => await sut.Evaluate(evaluationRequest); await act.Should().ThrowAsync(); } @@ -866,24 +910,27 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRespons var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new AuthZenBoxcarEvaluation() + + + Evaluations = new List() { - Subject = new AuthZenSubject + new AuthZenBoxcarEvaluation() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } } } }; - var defaults = new AuthZenBoxcarEvaluation(); - - var authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + var authZenResponse = await sut.Evaluate(evaluationRequest); authZenResponse.CorrelationId.Should().Be(expectedRequestId); } @@ -902,40 +949,42 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload { - Evaluations = new List + Payload = new AuthZenBoxcarEvaluationRequest() { - new() + Evaluations = new List { - Subject = new AuthZenSubject + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() { - Id = "dasfgthb", - Type = "aerfbqret" + Name = "hjkldfgb" } } } }; - - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - await sut.Evaluate(evaluationRequest, defaults); + await sut.Evaluate(evaluationRequest); requestSent.Should().NotBeNull(); requestSent.Method.Should().Be(HttpMethod.Post); @@ -957,41 +1006,45 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload() { CorrelationId = Guid.NewGuid().ToString(), - Evaluations = new List + Payload = new AuthZenBoxcarEvaluationRequest() { - new() + + + Evaluations = new List { - Subject = new AuthZenSubject + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() { - Id = "dasfgthb", - Type = "aerfbqret" + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" } } } }; - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - - await sut.Evaluate(evaluationRequest, defaults); + await sut.Evaluate(evaluationRequest); requestSent.Headers .Should() @@ -1013,41 +1066,43 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload() { CorrelationId = Guid.NewGuid().ToString(), - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new() + Evaluations = new List() { - Subject = new AuthZenSubject + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() { - Id = "dasfgthb", - Type = "aerfbqret" + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" } } } }; - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - - await sut.Evaluate(evaluationRequest, defaults); + await sut.Evaluate(evaluationRequest); string sentContent = await requestSent.Content.ReadAsStringAsync(); @@ -1056,7 +1111,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh AdjustBoxcarRequestSerialization(deserializedRequest); - deserializedRequest.Should().BeEquivalentTo(evaluationRequest.ToDto(defaults)); + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Payload.ToDto()); } [Theory] @@ -1086,40 +1141,42 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest() + var evaluationRequest = new AuthZenPayload() { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new AuthZenBoxcarEvaluation() + Evaluations = new List() { - Subject = new AuthZenSubject - { - Id = "dasfgthb", - Type = "aerfbqret" - } + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } } } }; - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - - AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest); authZenResponse.Evaluations.Should().BeEquivalentTo(new List() { @@ -1172,46 +1229,49 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload() { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new AuthZenBoxcarEvaluation() + Evaluations = new List() { - Subject = new AuthZenSubject - { - Id = "dasfgthb", - Type = "aerfbqret" - } + new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } } } }; - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - - AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest); authZenResponse.Evaluations.Single().Context.Should().Be(context.Trim()); } - + [Fact] - public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAndRequestFails_ShouldThrowAuthZenRequestFailureException() + public async Task + Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAndRequestFails_ShouldThrowAuthZenRequestFailureException() { httpMessageHandler.Protected() .Setup>("SendAsync", ItExpr.IsAny(), @@ -1220,40 +1280,42 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload() { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new AuthZenBoxcarEvaluation() + Evaluations = new List() { - Subject = new AuthZenSubject + new AuthZenBoxcarEvaluation() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" } } } }; - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - - Func act = async () => await sut.Evaluate(evaluationRequest, defaults); + Func act = async () => await sut.Evaluate(evaluationRequest); await act.Should().ThrowAsync(); } @@ -1282,40 +1344,42 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload() { - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new AuthZenBoxcarEvaluation() + Evaluations = new List() { - Subject = new AuthZenSubject + new AuthZenBoxcarEvaluation() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" } } } }; - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - - var authZenResponse = await sut.Evaluate(evaluationRequest, defaults); + var authZenResponse = await sut.Evaluate(evaluationRequest); authZenResponse.CorrelationId.Should().Be(expectedRequestId); } @@ -1337,46 +1401,47 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload() { CorrelationId = Guid.NewGuid().ToString(), - Evaluations = new List() + Payload = new AuthZenBoxcarEvaluationRequest() { - new() + Evaluations = new List() { - Subject = new AuthZenSubject + new() { - Id = "dasfgthb", - Type = "aerfbqret" + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } } + }, + DefaultValues = new AuthZenBoxcarEvaluation() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + }, + Options = new AuthZenBoxcarOptions + { + Semantics = semantics } } }; - var defaults = new AuthZenBoxcarEvaluation() - { - Subject = new AuthZenSubject() - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource() - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction() - { - Name = "hjkldfgb" - } - }; - - var boxcarOptions = new AuthZenBoxcarOptions - { - Semantics = semantics - }; - - await sut.Evaluate(evaluationRequest, defaults, boxcarOptions); + await sut.Evaluate(evaluationRequest); string sentContent = await requestSent.Content.ReadAsStringAsync(); @@ -1385,7 +1450,7 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS AdjustBoxcarRequestSerialization(deserializedRequest); - var expectation = evaluationRequest.ToDto(defaults, boxcarOptions); + var expectation = evaluationRequest.Payload.ToDto(); deserializedRequest.Should().BeEquivalentTo(expectation); } @@ -1403,30 +1468,32 @@ public async Task Evaluate_WhenBoxCarEvaluationsIsMissing_ShouldFallbackToSingle var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarRequest + var evaluationRequest = new AuthZenPayload() { - Evaluations = new List() - }; - - var defaults = new AuthZenBoxcarEvaluation - { - Subject = new AuthZenSubject - { - Id = "jk;dfgn", - Type = "jk;dfbgn;" - }, - Resource = new AuthZenResource - { - Type = "hjldfbg", - Id = "jkldfbgns" - }, - Action = new AuthZenAction + Payload = new AuthZenBoxcarEvaluationRequest() { - Name = "hjkldfgb" + Evaluations = new List(), + DefaultValues = new AuthZenBoxcarEvaluation + { + Subject = new AuthZenSubject + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction + { + Name = "hjkldfgb" + } + } } }; - await sut.Evaluate(evaluationRequest, defaults); + await sut.Evaluate(evaluationRequest); requestSent.Should().NotBeNull(); requestSent.Method.Should().Be(HttpMethod.Post); diff --git a/Rsk.AuthZen.Client.Test/AuthZenRequestBuilderTests.cs b/Rsk.AuthZen.Client.Test/AuthZenRequestBuilderTests.cs index fbb96ee..0031d2b 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenRequestBuilderTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenRequestBuilderTests.cs @@ -161,31 +161,31 @@ public void Build_WhenCalled_ShouldConstructRequestCorrectly() .Add(contextProperty2, contextProperty2Value) .Add(contextProperty3, contextProperty3Value); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); result.Should().NotBeNull(); - result.Subject.Id.Should().Be(subjectId); - result.Subject.Type.Should().Be(subjectType); - result.Subject.Properties.Should().HaveCount(2); - result.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty1 && kv.Value.Equals(subjectProperty1Value)); - result.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty2 && kv.Value.Equals(subjectProperty2Value)); - - result.Resource.Id.Should().Be(resourceId); - result.Resource.Type.Should().Be(resourceType); - result.Resource.Properties.Should().HaveCount(2); - result.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty1 && kv.Value.Equals(resourceProperty1Value)); - result.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty2 && kv.Value.Equals(resourceProperty2Value)); - - result.Action.Name.Should().Be(actionName); - result.Action.Properties.Should().HaveCount(2); - result.Action.Properties.Should().Contain(kv => kv.Key == actionProperty1 && kv.Value.Equals(actionProperty1Value)); - result.Action.Properties.Should().Contain(kv => kv.Key == actionProperty2 && kv.Value.Equals(actionProperty2Value)); + result.Payload.Subject.Id.Should().Be(subjectId); + result.Payload.Subject.Type.Should().Be(subjectType); + result.Payload.Subject.Properties.Should().HaveCount(2); + result.Payload.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty1 && kv.Value.Equals(subjectProperty1Value)); + result.Payload.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty2 && kv.Value.Equals(subjectProperty2Value)); + + result.Payload.Resource.Id.Should().Be(resourceId); + result.Payload.Resource.Type.Should().Be(resourceType); + result.Payload.Resource.Properties.Should().HaveCount(2); + result.Payload.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty1 && kv.Value.Equals(resourceProperty1Value)); + result.Payload.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty2 && kv.Value.Equals(resourceProperty2Value)); + + result.Payload.Action.Name.Should().Be(actionName); + result.Payload.Action.Properties.Should().HaveCount(2); + result.Payload.Action.Properties.Should().Contain(kv => kv.Key == actionProperty1 && kv.Value.Equals(actionProperty1Value)); + result.Payload.Action.Properties.Should().Contain(kv => kv.Key == actionProperty2 && kv.Value.Equals(actionProperty2Value)); - result.Context.Should().HaveCount(3); - result.Context.Should().Contain(kv => kv.Key == contextProperty1 && kv.Value.Equals(contextProperty1Value)); - result.Context.Should().Contain(kv => kv.Key == contextProperty2 && kv.Value.Equals(contextProperty2Value)); - result.Context.Should().Contain(kv => kv.Key == contextProperty3 && kv.Value.Equals(contextProperty3Value)); + result.Payload.Context.Should().HaveCount(3); + result.Payload.Context.Should().Contain(kv => kv.Key == contextProperty1 && kv.Value.Equals(contextProperty1Value)); + result.Payload.Context.Should().Contain(kv => kv.Key == contextProperty2 && kv.Value.Equals(contextProperty2Value)); + result.Payload.Context.Should().Contain(kv => kv.Key == contextProperty3 && kv.Value.Equals(contextProperty3Value)); } [Fact] @@ -202,9 +202,9 @@ public void Build_WhenCalledWithNoSubject_ShouldNotSetSubject() sut.SetContext() .Add("iuhdcv8", 76.5m); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Subject.Should().BeNull(); + result.Payload.Subject.Should().BeNull(); } [Fact] @@ -221,9 +221,9 @@ public void Build_WhenCalledWithNoResource_ShouldNotSetResource() sut.SetContext() .Add("iuhdcv8", 76.5m); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Resource.Should().BeNull(); + result.Payload.Resource.Should().BeNull(); } [Fact] @@ -240,9 +240,9 @@ public void Build_WhenCalledWithNoAction_ShouldNotSetAction() sut.SetContext() .Add("iuhdcv8", 76.5m); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Action.Should().BeNull(); + result.Payload.Action.Should().BeNull(); } [Fact] @@ -259,9 +259,9 @@ public void Build_WhenCalledWithNoContext_ShouldNotSetContext() sut.SetAction("oihsxdfbvh") .Add("iuhdcv8", 76.5m); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Context.Should().BeNull(); + result.Payload.Context.Should().BeNull(); } [Fact] @@ -271,9 +271,9 @@ public void Build_WhenCalledWithSubjectWithNoProperties_ShouldNotSetSubjectPrope sut.SetSubject("lkjshdv", "loikjhv"); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Subject.Properties.Should().BeNull(); + result.Payload.Subject.Properties.Should().BeNull(); } [Fact] @@ -283,9 +283,9 @@ public void Build_WhenCalledWithResourceWithNoProperties_ShouldNotSetResourcePro sut.SetResource("lkjshdv", "loikjhv"); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Resource.Properties.Should().BeNull(); + result.Payload.Resource.Properties.Should().BeNull(); } [Fact] @@ -295,9 +295,9 @@ public void Build_WhenCalledWithActionWithNoProperties_ShouldNotSetActionPropert sut.SetAction("lkjshdv"); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Action.Properties.Should().BeNull(); + result.Payload.Action.Properties.Should().BeNull(); } [Fact] @@ -307,9 +307,9 @@ public void Build_WhenCalledWithContextWithNoProperties_ShouldNotSetContext() sut.SetContext(); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); - result.Context.Should().BeNull(); + result.Payload.Context.Should().BeNull(); } [Fact] @@ -344,7 +344,7 @@ public void Build_WhenCalledWithCorrelationId_ShouldSetCorrelationId() sut.SetCorrelationId(expectedCorrelationId); - AuthZenEvaluationRequest result = sut.Build(); + var result = sut.Build(); result.CorrelationId.Should().Be(expectedCorrelationId); } diff --git a/Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs similarity index 98% rename from Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs rename to Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs index 47d2fc3..8b4dcf5 100644 --- a/Rsk.AuthZen.Client/AuthZenBoxcarRequest.cs +++ b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs @@ -4,12 +4,10 @@ namespace Rsk.AuthZen.Client { - public class AuthZenBoxcarRequest + public class AuthZenBoxcarEvaluationRequest { public List Evaluations { get; internal set; } - public AuthZenBoxcarEvaluation DefaultValues { get; internal set; } - public AuthZenBoxcarOptions Options { get; internal set; } internal AuthZenBoxcarRequestMessageDto ToDto() diff --git a/Rsk.AuthZen.Client/AuthZenClient.cs b/Rsk.AuthZen.Client/AuthZenClient.cs index 554bdc5..c0b5c8b 100644 --- a/Rsk.AuthZen.Client/AuthZenClient.cs +++ b/Rsk.AuthZen.Client/AuthZenClient.cs @@ -42,15 +42,15 @@ public AuthZenClient(IHttpClientFactory httpClientFactory, IOptions Evaluate(AuthZenEvaluationRequest evaluationRequest) + public async Task Evaluate(AuthZenPayload request) { var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{EvaluationUri}", UriKind.Relative)); - if (evaluationRequest.CorrelationId != null) + if (request.CorrelationId != null) { - requestMessage.Headers.Add(RequestIdHeader, evaluationRequest.CorrelationId); + requestMessage.Headers.Add(RequestIdHeader, request.CorrelationId); } - string requestJson = JsonSerializer.Serialize(evaluationRequest.ToDto(), serializerOptions); + string requestJson = JsonSerializer.Serialize(request.Payload.ToDto(), serializerOptions); HttpContent content = new StringContent(requestJson, Encoding.UTF8, AuthZenContentType); requestMessage.Content = content; @@ -79,16 +79,11 @@ public async Task Evaluate(AuthZenEvaluationRequest evaluationR return authZenResponse; } - public Task Evaluate(AuthZenBoxcarRequest request, AuthZenBoxcarEvaluation defaults) - { - return Evaluate(request, defaults, null); - } - - public async Task Evaluate(AuthZenBoxcarRequest request, AuthZenBoxcarEvaluation defaults, AuthZenBoxcarOptions boxcarOptions) + public async Task Evaluate(AuthZenPayload request) { if (IsMultiEvaluationsMissing(request)) { - return await FallbackToSingleEvaluation(request, defaults); + return await FallbackToSingleEvaluation(request); } var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{BoxcarUri}", UriKind.Relative)); @@ -98,7 +93,7 @@ public async Task Evaluate(AuthZenBoxcarRequest request, requestMessage.Headers.Add(RequestIdHeader, request.CorrelationId); } - string requestJson = JsonSerializer.Serialize(request.ToDto(defaults, boxcarOptions), serializerOptions); + string requestJson = JsonSerializer.Serialize(request.Payload.ToDto(), serializerOptions); HttpContent content = new StringContent(requestJson, Encoding.UTF8, AuthZenContentType); requestMessage.Content = content; @@ -131,20 +126,23 @@ public async Task Evaluate(AuthZenBoxcarRequest request, return response; } - private static bool IsMultiEvaluationsMissing(AuthZenBoxcarRequest request) + private static bool IsMultiEvaluationsMissing(AuthZenPayload evaluationRequest) { - return request.Evaluations == null || !request.Evaluations.Any(); + return evaluationRequest.Payload.Evaluations == null || !evaluationRequest.Payload.Evaluations.Any(); } - private async Task FallbackToSingleEvaluation(AuthZenBoxcarRequest request, AuthZenBoxcarEvaluation defaults) + private async Task FallbackToSingleEvaluation(AuthZenPayload evaluationRequest) { - var singleResponse = await Evaluate(new AuthZenEvaluationRequest() + var singleResponse = await Evaluate(new AuthZenPayload() { - Context = defaults.Context, - Subject = defaults.Subject, - Resource = defaults.Resource, - Action = defaults.Action, - CorrelationId = request.CorrelationId + CorrelationId = evaluationRequest.CorrelationId, + Payload = new AuthZenSingleEvaluationRequest + { + Context = evaluationRequest.Payload.DefaultValues.Context, + Subject = evaluationRequest.Payload.DefaultValues.Subject, + Resource = evaluationRequest.Payload.DefaultValues.Resource, + Action = evaluationRequest.Payload.DefaultValues.Action, + } }); return new AuthZenBoxcarResponse diff --git a/Rsk.AuthZen.Client/AuthZenPayload.cs b/Rsk.AuthZen.Client/AuthZenPayload.cs index 39177c7..0dd8754 100644 --- a/Rsk.AuthZen.Client/AuthZenPayload.cs +++ b/Rsk.AuthZen.Client/AuthZenPayload.cs @@ -2,7 +2,7 @@ namespace Rsk.AuthZen.Client { public class AuthZenPayload { - public string CorrelationId { get; set; } - public T Payload { get; set; } + public string CorrelationId { get; internal set; } + public T Payload { get; internal set; } } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenRequestBuilder.cs b/Rsk.AuthZen.Client/AuthZenRequestBuilder.cs index efcf8c6..250a819 100644 --- a/Rsk.AuthZen.Client/AuthZenRequestBuilder.cs +++ b/Rsk.AuthZen.Client/AuthZenRequestBuilder.cs @@ -53,9 +53,9 @@ public IAuthZenPropertyBag SetContext() return contextProperties; } - public AuthZenEvaluationRequest Build() + public AuthZenPayload Build() { - var request = new AuthZenEvaluationRequest(); + var request = new AuthZenSingleEvaluationRequest(); if (subjectId != null) { @@ -103,12 +103,11 @@ public AuthZenEvaluationRequest Build() request.Context = contextProperties.Build(); } - if (correlationId != null) + return new AuthZenPayload { - request.CorrelationId = correlationId; - } - - return request; + Payload = request, + CorrelationId = correlationId + }; } public IAuthZenRequestBuilder SetCorrelationId(string id) diff --git a/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs b/Rsk.AuthZen.Client/AuthZenSingleEvaluationRequest.cs similarity index 96% rename from Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs rename to Rsk.AuthZen.Client/AuthZenSingleEvaluationRequest.cs index ae635ac..916adca 100644 --- a/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs +++ b/Rsk.AuthZen.Client/AuthZenSingleEvaluationRequest.cs @@ -3,7 +3,7 @@ namespace Rsk.AuthZen.Client { - public class AuthZenEvaluationRequest + public class AuthZenSingleEvaluationRequest { public AuthZenSubject Subject { get; internal set; } public AuthZenResource Resource { get; internal set; } diff --git a/Rsk.AuthZen.Client/IAuthZenClient.cs b/Rsk.AuthZen.Client/IAuthZenClient.cs index e9f5280..03c8a50 100644 --- a/Rsk.AuthZen.Client/IAuthZenClient.cs +++ b/Rsk.AuthZen.Client/IAuthZenClient.cs @@ -6,14 +6,9 @@ namespace Rsk.AuthZen.Client public interface IAuthZenClient { Task Evaluate( - AuthZenPayload evaluationRequest); + AuthZenPayload request); Task Evaluate( - AuthZenPayload request); - - // Task Evaluate( - // AuthZenBoxcarRequest request, - // AuthZenBoxcarEvaluation defaults, - // AuthZenBoxcarOptions boxcarOptions); + AuthZenPayload request); } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs index db6dcbd..fa6df77 100644 --- a/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs +++ b/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs @@ -10,7 +10,7 @@ public interface IAuthZenRequestBuilder IAuthZenPropertyBag SetContext(); IAuthZenRequestBuilder SetCorrelationId(string correlationId); - AuthZenPayload Build(); + AuthZenPayload Build(); } public interface IAuthZenPropertyBag From 87f1ed360ba7803e56b7df58da76e9e4921dd272 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Fri, 20 Jun 2025 14:46:22 +0100 Subject: [PATCH 03/33] Boxcar request builder --- .../AuthZenBoxcarEvaluationRequestTests.cs | 22 +- .../AuthZenBoxcarRequestBuilderTests.cs | 678 ++++++++++++++++++ Rsk.AuthZen.Client.Test/AuthZenClientTests.cs | 118 +-- ...cs => AuthZenSingleRequestBuilderTests.cs} | 8 +- .../AuthZenBoxcarEvaluationRequest.cs | 50 +- Rsk.AuthZen.Client/AuthZenClient.cs | 6 +- ...Request.cs => AuthZenEvaluationRequest.cs} | 2 +- ...lder.cs => AuthZenSingleRequestBuilder.cs} | 29 +- .../IAuthZenBoxcarRequestBuilder.cs | 287 ++++++++ Rsk.AuthZen.Client/IAuthZenClient.cs | 2 +- ...der.cs => IAuthZenSingleRequestBuilder.cs} | 8 +- 11 files changed, 1067 insertions(+), 143 deletions(-) create mode 100644 Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs rename Rsk.AuthZen.Client.Test/{AuthZenRequestBuilderTests.cs => AuthZenSingleRequestBuilderTests.cs} (97%) rename Rsk.AuthZen.Client/{AuthZenSingleEvaluationRequest.cs => AuthZenEvaluationRequest.cs} (96%) rename Rsk.AuthZen.Client/{AuthZenRequestBuilder.cs => AuthZenSingleRequestBuilder.cs} (91%) create mode 100644 Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs rename Rsk.AuthZen.Client/{IAuthZenRequestBuilder.cs => IAuthZenSingleRequestBuilder.cs} (83%) diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs b/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs index 6a03dcd..5c8e434 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs @@ -9,7 +9,7 @@ public class AuthZenBoxcarEvaluationRequestTests [Fact] public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() { - var defaults = new AuthZenBoxcarEvaluation() + var defaults = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -37,7 +37,7 @@ public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() [Fact] public void ToDto_WhenDefaultResourceIsSet_ShouldPopulateResource() { - var defaults = new AuthZenBoxcarEvaluation() + var defaults = new AuthZenEvaluationRequest() { Resource = new AuthZenResource { @@ -64,7 +64,7 @@ public void ToDto_WhenDefaultResourceIsSet_ShouldPopulateResource() [Fact] public void ToDto_WhenDefaultActionIsSet_ShouldPopulateAction() { - var defaults = new AuthZenBoxcarEvaluation() + var defaults = new AuthZenEvaluationRequest() { Action = new AuthZenAction { @@ -89,7 +89,7 @@ public void ToDto_WhenDefaultActionIsSet_ShouldPopulateAction() [Fact] public void ToDto_WhenDefaultContextIsSet_ShouldPopulateContext() { - var defaults = new AuthZenBoxcarEvaluation() + var defaults = new AuthZenEvaluationRequest() { Context = new Dictionary { @@ -127,7 +127,7 @@ public void ToDto_WhenEvaluationsIsEmpty_ShouldNotPopulateEvaluations() { var request = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List() + Evaluations = new List() }; var dto = request.ToDto(); @@ -140,7 +140,7 @@ public void ToDto_WhenEvaluationsIsSet_ShouldPopulateEachEvaluation() { var request = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List + Evaluations = new List { new () { @@ -194,7 +194,7 @@ public void ToDto_WhenEvaluationSubjectIsSet_ShouldPopulateSubject() { var request = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List() + Evaluations = new List() { new () { @@ -222,7 +222,7 @@ public void ToDto_WhenEvaluationResourceIsSet_ShouldPopulateResource() { var request = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List() + Evaluations = new List() { new () { @@ -251,7 +251,7 @@ public void ToDto_WhenEvaluationActionIsSet_ShouldPopulateAction() { var request = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List() + Evaluations = new List() { new () { @@ -277,7 +277,7 @@ public void ToDto_WhenEvaluationContextIsSet_ShouldPopulateContext() { var request = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List() + Evaluations = new List() { new () { @@ -309,7 +309,7 @@ public void ToDto_OptionsAreProvided_ShouldIncludeOptionsInRequestDto(BoxcarSema var request = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List + Evaluations = new List { new () { diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs b/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs new file mode 100644 index 0000000..2b0ecd9 --- /dev/null +++ b/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs @@ -0,0 +1,678 @@ +using System; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Rsk.AuthZen.Client.Test; + +public class AuthZenBoxcarRequestBuilderTests +{ + private AuthZenBoxcarRequestBuilder CreateSut() + { + return new AuthZenBoxcarRequestBuilder(); + } + + [Fact] + public void SetCorrelationId_WhenCalled_ShouldReturnSameBuilderInstance() + { + var sut = CreateSut(); + + var result = sut.SetCorrelationId("test-correlation-id"); + + result.Should().BeSameAs(sut); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void SetCorrelationId_WhenCalledWithNullOrEmpty_ShouldThrowArgumentException(string correlationId) + { + var sut = CreateSut(); + + Action act = () => sut.SetCorrelationId(correlationId); + + act.Should().Throw() + .WithMessage("Correlation ID must be provided*") + .WithParameterName("id"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void SetDefaultSubject_WhenCalledWithNullOrEmptyId_ShouldThrowArgumentException(string value) + { + var sut = CreateSut(); + + Action act = () => sut.SetDefaultSubject(value, "hjdfgbhjdg"); + + act.Should().Throw() + .WithMessage("ID must be provided*") + .WithParameterName("id"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void SetDefaultSubject_WhenCalledWithNullOrEmptyType_ShouldThrowArgumentException(string value) + { + var sut = CreateSut(); + + Action act = () => sut.SetDefaultSubject("mhdfbghd",value); + + act.Should().Throw() + .WithMessage("Type must be provided*") + .WithParameterName("type"); + } + + [Fact] + public void SetDefaultSubject_WhenCalled_ShouldReturnEmptyPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.SetDefaultSubject("subject-id", "subject-type"); + + result.Should().NotBeNull(); + result.IsEmpty.Should().BeTrue(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void SetDefaultResource_WhenCalledWithNullOrEmptyId_ShouldThrowArgumentException(string value) + { + var sut = CreateSut(); + + Action act = () => sut.SetDefaultResource(value, "resource-type"); + + act.Should().Throw() + .WithMessage("ID must be provided*") + .WithParameterName("id"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void SetDefaultResource_WhenCalledWithNullOrEmptyType_ShouldThrowArgumentException(string value) + { + var sut = CreateSut(); + + Action act = () => sut.SetDefaultResource("resource-id", value); + + act.Should().Throw() + .WithMessage("Type must be provided*") + .WithParameterName("type"); + } + + [Fact] + public void SetDefaultResource_WhenCalled_ShouldReturnEmptyPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.SetDefaultResource("resource-id", "resource-type"); + + result.Should().NotBeNull(); + result.IsEmpty.Should().BeTrue(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void SetDefaultAction_WhenCalledWithNullOrEmptyName_ShouldThrowArgumentException(string value) + { + var sut = CreateSut(); + + Action act = () => sut.SetDefaultAction(value); + + act.Should().Throw() + .WithMessage("Name must be provided*") + .WithParameterName("name"); + } + + [Fact] + public void SetDefaultAction_WhenCalled_ShouldReturnEmptyPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.SetDefaultAction("action-name"); + + result.Should().NotBeNull(); + result.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void SetDefaultContext_WhenCalled_ShouldReturnPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.SetDefaultContext(); + + result.Should().NotBeNull(); + } + + [Fact] + public void Build_WhenCalledWithAllDefaults_ShouldCreateCorrectRequest() + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultSubject("subject-id", "subject-type"); + sut.SetDefaultResource("resource-id", "resource-type"); + sut.SetDefaultAction("action-name"); + sut.SetDefaultContext() + .Add("dfhjgbdfg", "dhjfbgdjhg"); + + var request = sut.Build(); + + request.Should().NotBeNull(); + request.CorrelationId.Should().Be("test-correlation-id"); + request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); + request.Payload.DefaultValues.Context.Should().NotBeNull(); + } + + [Fact] + public void Build_WhenCalledWithMissingDefaultSubject_ShouldBuildCorrectRequest() + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultResource("resource-id", "resource-type"); + sut.SetDefaultAction("action-name"); + sut.SetDefaultContext() + .Add("dfhjgbdfg", "dhjfbgdjhg"); + + var request = sut.Build(); + + request.Should().NotBeNull(); + request.CorrelationId.Should().Be("test-correlation-id"); + request.Payload.DefaultValues.Subject.Should().BeNull(); + request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); + request.Payload.DefaultValues.Context.Should().NotBeNull(); + } + + [Fact] + public void Build_WhenCalledWithMissingDefaultResource_ShouldBuildCorrectRequest() + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultSubject("subject-id", "subject-type"); + sut.SetDefaultAction("action-name"); + sut.SetDefaultContext() + .Add("dfhjgbdfg", "dhjfbgdjhg"); + + var request = sut.Build(); + + request.Should().NotBeNull(); + request.CorrelationId.Should().Be("test-correlation-id"); + request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Payload.DefaultValues.Resource.Should().BeNull(); + request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); + request.Payload.DefaultValues.Context.Should().NotBeNull(); + } + + [Fact] + public void Build_WhenCalledWithMissingDefaultAction_ShouldBuildCorrectRequest() + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultSubject("subject-id", "subject-type"); + sut.SetDefaultResource("resource-id", "resource-type"); + sut.SetDefaultContext() + .Add("dfhjgbdfg", "dhjfbgdjhg"); + + var request = sut.Build(); + + request.Should().NotBeNull(); + request.CorrelationId.Should().Be("test-correlation-id"); + request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Payload.DefaultValues.Action.Should().BeNull(); + request.Payload.DefaultValues.Context.Should().NotBeNull(); + } + + [Fact] + public void Build_WhenCalledWithMissingDefaultContext_ShouldBuildCorrectRequest() + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultSubject("subject-id", "subject-type"); + sut.SetDefaultResource("resource-id", "resource-type"); + sut.SetDefaultAction("action-name"); + + var request = sut.Build(); + + request.Should().NotBeNull(); + request.CorrelationId.Should().Be("test-correlation-id"); + request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); + request.Payload.DefaultValues.Context.Should().BeNull(); + } + [Fact] + public void Build_WhenCalledWithEmptyDefaultContext_ShouldBuildCorrectRequest() + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultSubject("subject-id", "subject-type"); + sut.SetDefaultResource("resource-id", "resource-type"); + sut.SetDefaultAction("action-name"); + sut.SetDefaultContext(); + + var request = sut.Build(); + + request.Should().NotBeNull(); + request.CorrelationId.Should().Be("test-correlation-id"); + request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); + request.Payload.DefaultValues.Context.Should().BeNull(); + } + + [Fact] + public void AddRequest_WhenCalled_ShouldShouldReturnRequestBuilder() + { + var sut = CreateSut(); + + var result = sut.AddRequest(); + + result.Should().NotBeSameAs(sut); + result.Should().BeAssignableTo(); + } + + [Fact] + public void AddRequestThenSetSubject_WhenCalled_ShouldReturnAnIAuthZenPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.AddRequest().SetSubject("id", "type"); + + result.Should().NotBeNull(); + } + + [Fact] + public void AddRequestThenSetResource_WhenCalled_ShouldReturnAnIAuthZenPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.AddRequest().SetResource("id", "type"); + + result.Should().NotBeNull(); + } + + [Fact] + public void AddRequestThenSetAction_WhenCalled_ShouldReturnAnIAuthZenPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.AddRequest().SetAction("dfbgde"); + + result.Should().NotBeNull(); + } + + [Fact] + public void AddRequestThenSetContext_WhenCalled_ShouldReturnAnIAuthZenPropertyBag() + { + var sut = CreateSut(); + + IAuthZenPropertyBag result = sut.AddRequest().SetContext(); + + result.Should().NotBeNull(); + } + + [Theory] + [InlineData(null)] + [InlineData(" ")] + [InlineData("")] + public void AddRequestThenSetSubject_WhenCalledWithInvalidId_ShouldThrowArgumentException(string invalidId) + { + var sut = CreateSut(); + + Action act = () => sut.AddRequest().SetSubject(invalidId, "dihg"); + + act.Should().Throw(); + } + + [Theory] + [InlineData(null)] + [InlineData(" ")] + [InlineData("")] + public void AddRequestThenSetSubject_WhenCalledWithInvalidType_ShouldThrowArgumentException(string invalidType) + { + var sut = CreateSut(); + + Action act = () => sut.AddRequest().SetSubject("iousdhgb", invalidType); + + act.Should().Throw(); + } + + [Theory] + [InlineData(null)] + [InlineData(" ")] + [InlineData("")] + public void AddRequestThenSetResource_WhenCalledWithInvalidId_ShouldThrowArgumentException(string invalidId) + { + var sut = CreateSut(); + + Action act = () => sut.AddRequest().SetResource(invalidId, "dihg"); + + act.Should().Throw(); + } + + [Theory] + [InlineData(null)] + [InlineData(" ")] + [InlineData("")] + public void AddRequestThenSetResource_WhenCalledWithInvalidType_ShouldThrowArgumentException(string invalidType) + { + var sut = CreateSut(); + + Action act = () => sut.AddRequest().SetResource("iousdhgb", invalidType); + + act.Should().Throw(); + } + + [Theory] + [InlineData(null)] + [InlineData(" ")] + [InlineData("")] + public void AddRequestThenSetAction_WhenCalledWithInvalidName_ShouldThrowArgumentException(string invalidName) + { + var sut = CreateSut(); + + Action act = () => sut.AddRequest().SetAction(invalidName); + + act.Should().Throw(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalled_ShouldConstructRequestCorrectly() + { + string subjectId = "jkisdhbfgv"; + string subjectType = "sdfg"; + string resourceId = "kjihsdfghui"; + string resourceType = "kijhi"; + string actionName = "kijh"; + + string subjectProperty1 = "ijsbdgvu"; + int subjectProperty1Value = 8; + string subjectProperty2 = "ksjhdfgo"; + bool subjectProperty2Value = true; + + string resourceProperty1 = "sfgnwse"; + DateTime resourceProperty1Value = new DateTime(2020, 12, 31); + string resourceProperty2 = "bneafn"; + decimal resourceProperty2Value = 7826; + + string actionProperty1 = "fsgns"; + string actionProperty1Value = "xdsjhvb"; + string actionProperty2 = "hbeashb"; + int actionProperty2Value = 81; + + string contextProperty1 = "sfgnsfg"; + int contextProperty1Value = 87; + string contextProperty2 = "bfesd"; + string contextProperty2Value = "khuibsdfv"; + string contextProperty3 = "saihdf90"; + string contextProperty3Value = "loihhio"; + + var sut = CreateSut(); + + var evaluation = sut.AddRequest(); + + evaluation.SetSubject(subjectId, subjectType) + .Add(subjectProperty1, subjectProperty1Value) + .Add(subjectProperty2, subjectProperty2Value); + + evaluation.SetResource(resourceId, resourceType) + .Add(resourceProperty1, resourceProperty1Value) + .Add(resourceProperty2, resourceProperty2Value); + + evaluation.SetAction(actionName) + .Add(actionProperty1, actionProperty1Value) + .Add(actionProperty2, actionProperty2Value); + + evaluation.SetContext() + .Add(contextProperty1, contextProperty1Value) + .Add(contextProperty2, contextProperty2Value) + .Add(contextProperty3, contextProperty3Value); + + var result = sut.Build(); + + result.Should().NotBeNull(); + + result.Payload.Evaluations.Single().Subject.Id.Should().Be(subjectId); + result.Payload.Evaluations.Single().Subject.Type.Should().Be(subjectType); + result.Payload.Evaluations.Single().Subject.Properties.Should().HaveCount(2); + result.Payload.Evaluations.Single().Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty1 && kv.Value.Equals(subjectProperty1Value)); + result.Payload.Evaluations.Single().Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty2 && kv.Value.Equals(subjectProperty2Value)); + + result.Payload.Evaluations.Single().Resource.Id.Should().Be(resourceId); + result.Payload.Evaluations.Single().Resource.Type.Should().Be(resourceType); + result.Payload.Evaluations.Single().Resource.Properties.Should().HaveCount(2); + result.Payload.Evaluations.Single().Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty1 && kv.Value.Equals(resourceProperty1Value)); + result.Payload.Evaluations.Single().Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty2 && kv.Value.Equals(resourceProperty2Value)); + + result.Payload.Evaluations.Single().Action.Name.Should().Be(actionName); + result.Payload.Evaluations.Single().Action.Properties.Should().HaveCount(2); + result.Payload.Evaluations.Single().Action.Properties.Should().Contain(kv => kv.Key == actionProperty1 && kv.Value.Equals(actionProperty1Value)); + result.Payload.Evaluations.Single().Action.Properties.Should().Contain(kv => kv.Key == actionProperty2 && kv.Value.Equals(actionProperty2Value)); + + result.Payload.Evaluations.Single().Context.Should().HaveCount(3); + result.Payload.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty1 && kv.Value.Equals(contextProperty1Value)); + result.Payload.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty2 && kv.Value.Equals(contextProperty2Value)); + result.Payload.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty3 && kv.Value.Equals(contextProperty3Value)); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithNoSubject_ShouldNotSetSubject() + { + var sut = CreateSut(); + + var evaluation = sut.AddRequest(); + + evaluation.SetResource("lkjshdv", "loikjhv") + .Add("lksiohv", "iuhgvi"); + + evaluation.SetAction("kjhvbsdcbiu") + .Add("ubsdfvubidxscfjib", 8); + + evaluation.SetContext() + .Add("iuhdcv8", 76.5m); + + var result = sut.Build(); + + result.Payload.Evaluations.Single().Subject.Should().BeNull(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithNoResource_ShouldNotSetResource() + { + var sut = CreateSut(); + + var evaluation = sut.AddRequest(); + + evaluation.SetSubject("lkjshdv", "loikjhv") + .Add("lksiohv", "iuhgvi"); + + evaluation.SetAction("kjhvbsdcbiu") + .Add("ubsdfvubidxscfjib", 8); + + evaluation.SetContext() + .Add("iuhdcv8", 76.5m); + + var result = sut.Build(); + + result.Payload.Evaluations.Single().Resource.Should().BeNull(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithNoAction_ShouldNotSetAction() + { + var sut = CreateSut(); + + var evaluation = sut.AddRequest(); + + evaluation.SetSubject("lkjshdv", "loikjhv") + .Add("lksiohv", "iuhgvi"); + + evaluation.SetResource("jkdf", "okji0p") + .Add("][-p0ik=", "oji-h9"); + + evaluation.SetContext() + .Add("iuhdcv8", 76.5m); + + var result = sut.Build(); + + result.Payload.Evaluations.Single().Action.Should().BeNull(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithNoContext_ShouldNotSetContext() + { + var sut = CreateSut(); + + var evaluation = sut.AddRequest(); + + evaluation.SetSubject("lkjshdv", "loikjhv") + .Add("lksiohv", "iuhgvi"); + + evaluation.SetResource("jkdf", "okji0p") + .Add("][-p0ik=", "oji-h9"); + + evaluation.SetAction("oihsxdfbvh") + .Add("iuhdcv8", 76.5m); + + var result = sut.Build(); + + result.Payload.Evaluations.Single().Context.Should().BeNull(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithSubjectWithNoProperties_ShouldNotSetSubjectProperties() + { + var sut = CreateSut(); + + sut.AddRequest().SetSubject("lkjshdv", "loikjhv"); + + var result = sut.Build(); + + result.Payload.Evaluations.Single().Subject.Properties.Should().BeNull(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithResourceWithNoProperties_ShouldNotSetResourceProperties() + { + var sut = CreateSut(); + + sut.AddRequest().SetResource("lkjshdv", "loikjhv"); + + var result = sut.Build(); + + result.Payload.Evaluations.Single().Resource.Properties.Should().BeNull(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithActionWithNoProperties_ShouldNotSetActionProperties() + { + var sut = CreateSut(); + + sut.AddRequest().SetAction("lkjshdv"); + + var result = sut.Build(); + + result.Payload.Evaluations.Single().Action.Properties.Should().BeNull(); + } + + [Fact] + public void AddRequestThenBuild_WhenCalledWithContextWithNoProperties_ShouldNotSetContext() + { + var sut = CreateSut(); + + sut.AddRequest().SetContext(); + + var result = sut.Build(); + + result.Payload.Evaluations.Should().BeEmpty(); + } + + [Theory] + [InlineData(BoxcarSemantics.DenyOnFirstDeny)] + [InlineData(BoxcarSemantics.PermitOnFirstPermit)] + [InlineData(BoxcarSemantics.ExecuteAll)] + public void SetEvaluationSemantics_WhenCalled_ShouldReturnSelf(BoxcarSemantics semantics) + { + var sut = CreateSut(); + + var result = sut.SetEvaluationSemantics(semantics); + + result.Should().BeSameAs(sut); + } + + [Theory] + [InlineData(BoxcarSemantics.DenyOnFirstDeny)] + [InlineData(BoxcarSemantics.PermitOnFirstPermit)] + [InlineData(BoxcarSemantics.ExecuteAll)] + public void Build_WhenSemanticsIsSet_ShouldShouldIncludeSemantics(BoxcarSemantics semantics) + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultSubject("subject-id", "subject-type"); + sut.SetDefaultResource("resource-id", "resource-type"); + sut.SetDefaultAction("action-name"); + sut.SetDefaultContext() + .Add("dfhjgbdfg", "dhjfbgdjhg"); + sut.SetEvaluationSemantics(semantics); + + var request = sut.Build(); + + request.Should().NotBeNull(); + request.CorrelationId.Should().Be("test-correlation-id"); + request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); + request.Payload.DefaultValues.Context.Should().NotBeNull(); + request.Payload.Options.Semantics.Should().Be(semantics); + } + + [Fact] + public void Build_WhenBoxcarSemanticsIsNotSet_ShouldExcludeFromRequest() + { + var sut = CreateSut(); + + sut.SetCorrelationId("test-correlation-id"); + sut.SetDefaultSubject("subject-id", "subject-type"); + sut.SetDefaultResource("resource-id", "resource-type"); + sut.SetDefaultAction("action-name"); + sut.SetDefaultContext() + .Add("dfhjgbdfg", "dhjfbgdjhg"); + + var request = sut.Build(); + + request.Payload.Options.Should().BeNull(); + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs index 3d843b8..16831ea 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs @@ -60,7 +60,7 @@ private AuthZenClient CreateSut() return new AuthZenClient(httpClientFactory?.Object, options?.Object); } - private async Task VerifyMissingRequestPartOmitsElement(AuthZenPayload singleEvaluationRequest, string expectedMissingElement) + private async Task VerifyMissingRequestPartOmitsElement(AuthZenPayload singleEvaluationRequest, string expectedMissingElement) { HttpRequestMessage requestSent = null; httpMessageHandler.Protected() @@ -182,9 +182,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostToCorrectEnd var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -216,9 +216,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationWithCorrelationId_Shoul var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest + Payload = new AuthZenEvaluationRequest { Subject = new AuthZenSubject { @@ -249,9 +249,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRe var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -305,9 +305,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRe [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoSubject_ShouldNotSerializeSubject() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Resource = new AuthZenResource { @@ -342,9 +342,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoSubject_ShouldNotS [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoResource_ShouldNotSerializeResource() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -379,9 +379,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoResource_ShouldNot [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoAction_ShouldNotSerializeAction() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject @@ -417,9 +417,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoAction_ShouldNotSe [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoContext_ShouldNotSerializeContext() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -474,9 +474,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldParseDecisionCor var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -521,9 +521,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldExtractContextCo var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -548,9 +548,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndRequestFails_ShouldT var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -585,9 +585,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndResponseContainsRequ var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenPayload { - Payload = new AuthZenSingleEvaluationRequest() + Payload = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -620,7 +620,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List + Evaluations = new List { new() { @@ -631,7 +631,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() } }; @@ -663,7 +663,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrel CorrelationId = Guid.NewGuid().ToString(), Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List + Evaluations = new List { new() { @@ -674,7 +674,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrel } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() }, }; @@ -705,7 +705,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos CorrelationId = Guid.NewGuid().ToString(), Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { new() { @@ -716,7 +716,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() } }; @@ -763,9 +763,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPar { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -834,9 +834,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldExt { Payload = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -867,9 +867,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRequest { Payload = new AuthZenBoxcarEvaluationRequest { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -916,9 +916,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRespons { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -953,7 +953,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List + Evaluations = new List { new() { @@ -964,7 +964,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1013,7 +1013,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit { - Evaluations = new List + Evaluations = new List { new() { @@ -1024,7 +1024,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1071,7 +1071,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh CorrelationId = Guid.NewGuid().ToString(), Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { new() { @@ -1082,7 +1082,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1145,9 +1145,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -1156,7 +1156,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1233,9 +1233,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -1244,7 +1244,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1284,9 +1284,9 @@ public async Task { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -1295,7 +1295,7 @@ public async Task } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1348,9 +1348,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenBoxcarEvaluation() + new AuthZenEvaluationRequest() { Subject = new AuthZenSubject { @@ -1359,7 +1359,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1406,7 +1406,7 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS CorrelationId = Guid.NewGuid().ToString(), Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List() + Evaluations = new List() { new() { @@ -1417,7 +1417,7 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS } } }, - DefaultValues = new AuthZenBoxcarEvaluation() + DefaultValues = new AuthZenEvaluationRequest() { Subject = new AuthZenSubject() { @@ -1472,8 +1472,8 @@ public async Task Evaluate_WhenBoxCarEvaluationsIsMissing_ShouldFallbackToSingle { Payload = new AuthZenBoxcarEvaluationRequest() { - Evaluations = new List(), - DefaultValues = new AuthZenBoxcarEvaluation + Evaluations = new List(), + DefaultValues = new AuthZenEvaluationRequest { Subject = new AuthZenSubject { diff --git a/Rsk.AuthZen.Client.Test/AuthZenRequestBuilderTests.cs b/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs similarity index 97% rename from Rsk.AuthZen.Client.Test/AuthZenRequestBuilderTests.cs rename to Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs index 0031d2b..12f082f 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenRequestBuilderTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs @@ -4,7 +4,7 @@ namespace Rsk.AuthZen.Client.Test; -public class AuthZenRequestBuilderTests +public class AuthZenSingleRequestBuilderTests { [Fact] public void SetSubject_WhenCalled_ShouldReturnAnIAuthZenPropertyBag() @@ -318,7 +318,7 @@ public void SetCorrelationId_WhenCalled_ShouldReturnSelf() string expectedCorrelationId = "ihjubvsdlfvchusdiufvbidusb"; var sut = CreateSut(); - IAuthZenRequestBuilder result = sut.SetCorrelationId(expectedCorrelationId); + IAuthZenSingleRequestBuilder result = sut.SetCorrelationId(expectedCorrelationId); result.Should().BeSameAs(sut); } @@ -349,8 +349,8 @@ public void Build_WhenCalledWithCorrelationId_ShouldSetCorrelationId() result.CorrelationId.Should().Be(expectedCorrelationId); } - private AuthZenRequestBuilder CreateSut() + private AuthZenSingleRequestBuilder CreateSut() { - return new AuthZenRequestBuilder(); + return new AuthZenSingleRequestBuilder(); } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs index 8b4dcf5..3e6b502 100644 --- a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs +++ b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs @@ -6,8 +6,8 @@ namespace Rsk.AuthZen.Client { public class AuthZenBoxcarEvaluationRequest { - public List Evaluations { get; internal set; } - public AuthZenBoxcarEvaluation DefaultValues { get; internal set; } + public List Evaluations { get; internal set; } + public AuthZenEvaluationRequest DefaultValues { get; internal set; } public AuthZenBoxcarOptions Options { get; internal set; } internal AuthZenBoxcarRequestMessageDto ToDto() @@ -102,50 +102,4 @@ private static string ConvertSemantics(BoxcarSemantics semantics) }; } } - - public class AuthZenBoxcarEvaluation - { - public AuthZenSubject Subject { get; internal set; } - public AuthZenResource Resource { get; internal set; } - public AuthZenAction Action { get; internal set; } - public Dictionary Context { get; internal set; } - - internal AuthZenRequestMessageDto ToDto() - { - var dto = new AuthZenRequestMessageDto(); - - if (Subject != null) - { - dto.Subject = new AuthZenSubjectDto - { - Id = Subject.Id, - Type = Subject.Type, - Properties = Subject.Properties - }; - } - - if (Resource != null) - { - dto.Resource = new AuthZenResourceDto - { - Id = Resource.Id, - Type = Resource.Type, - Properties = Resource.Properties - }; - } - - if (Action != null) - { - dto.Action = new AuthZenActionDto - { - Name = Action.Name, - Properties = Action.Properties - }; - } - - dto.Context = Context; - - return dto; - } - } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenClient.cs b/Rsk.AuthZen.Client/AuthZenClient.cs index c0b5c8b..974001e 100644 --- a/Rsk.AuthZen.Client/AuthZenClient.cs +++ b/Rsk.AuthZen.Client/AuthZenClient.cs @@ -42,7 +42,7 @@ public AuthZenClient(IHttpClientFactory httpClientFactory, IOptions Evaluate(AuthZenPayload request) + public async Task Evaluate(AuthZenPayload request) { var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{EvaluationUri}", UriKind.Relative)); if (request.CorrelationId != null) @@ -133,10 +133,10 @@ private static bool IsMultiEvaluationsMissing(AuthZenPayload FallbackToSingleEvaluation(AuthZenPayload evaluationRequest) { - var singleResponse = await Evaluate(new AuthZenPayload() + var singleResponse = await Evaluate(new AuthZenPayload() { CorrelationId = evaluationRequest.CorrelationId, - Payload = new AuthZenSingleEvaluationRequest + Payload = new AuthZenEvaluationRequest { Context = evaluationRequest.Payload.DefaultValues.Context, Subject = evaluationRequest.Payload.DefaultValues.Subject, diff --git a/Rsk.AuthZen.Client/AuthZenSingleEvaluationRequest.cs b/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs similarity index 96% rename from Rsk.AuthZen.Client/AuthZenSingleEvaluationRequest.cs rename to Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs index 916adca..ae635ac 100644 --- a/Rsk.AuthZen.Client/AuthZenSingleEvaluationRequest.cs +++ b/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs @@ -3,7 +3,7 @@ namespace Rsk.AuthZen.Client { - public class AuthZenSingleEvaluationRequest + public class AuthZenEvaluationRequest { public AuthZenSubject Subject { get; internal set; } public AuthZenResource Resource { get; internal set; } diff --git a/Rsk.AuthZen.Client/AuthZenRequestBuilder.cs b/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs similarity index 91% rename from Rsk.AuthZen.Client/AuthZenRequestBuilder.cs rename to Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs index 250a819..1e0d369 100644 --- a/Rsk.AuthZen.Client/AuthZenRequestBuilder.cs +++ b/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs @@ -2,20 +2,30 @@ namespace Rsk.AuthZen.Client { - internal class AuthZenRequestBuilder : IAuthZenRequestBuilder + public class AuthZenSingleRequestBuilder : IAuthZenSingleRequestBuilder { + private string correlationId; + private string subjectId; private string subjectType; private string resourceId; private string resourceType; private string actionName; - private string correlationId; private AuthZenPropertyBag subjectProperties; private AuthZenPropertyBag resourceProperties; private AuthZenPropertyBag actionProperties; private AuthZenPropertyBag contextProperties; + public IAuthZenSingleRequestBuilder SetCorrelationId(string id) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Correlation ID must be provided", nameof(id)); + + correlationId = id; + + return this; + } + public IAuthZenPropertyBag SetSubject(string id, string type) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id must be provided", nameof(id)); @@ -53,9 +63,9 @@ public IAuthZenPropertyBag SetContext() return contextProperties; } - public AuthZenPayload Build() + public AuthZenPayload Build() { - var request = new AuthZenSingleEvaluationRequest(); + var request = new AuthZenEvaluationRequest(); if (subjectId != null) { @@ -103,20 +113,11 @@ public AuthZenPayload Build() request.Context = contextProperties.Build(); } - return new AuthZenPayload + return new AuthZenPayload { Payload = request, CorrelationId = correlationId }; } - - public IAuthZenRequestBuilder SetCorrelationId(string id) - { - if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Correlation ID must be provided", nameof(id)); - - correlationId = id; - - return this; - } } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs new file mode 100644 index 0000000..d3422f2 --- /dev/null +++ b/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Rsk.AuthZen.Client +{ + public interface IAuthZenBoxcarRequestBuilder + { + IAuthZenBoxcarRequestBuilder SetCorrelationId(string correlationId); + + IAuthZenPropertyBag SetDefaultSubject(string id, string type); + IAuthZenPropertyBag SetDefaultResource(string id, string type); + IAuthZenPropertyBag SetDefaultAction(string name); + IAuthZenPropertyBag SetDefaultContext(); + + IAuthZenRequestBuilder AddRequest(); + + AuthZenPayload Build(); + } + + public class AuthZenBoxcarRequestBuilder : IAuthZenBoxcarRequestBuilder + { + private string correlationId; + + private string defaultSubjectId; + private string defaultSubjectType; + private string defaultResourceId; + private string defaultResourceType; + private string defaultActionName; + + private AuthZenPropertyBag defaultSubjectProperties; + private AuthZenPropertyBag defaultResourceProperties; + private AuthZenPropertyBag defaultActionProperties; + private AuthZenPropertyBag defaultContextProperties; + + private List evaluationBuilders = new List(); + private AuthZenBoxcarOptions options; + + public IAuthZenBoxcarRequestBuilder SetCorrelationId(string id) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Correlation ID must be provided", nameof(id)); + + correlationId = id; + + return this; + } + + public IAuthZenPropertyBag SetDefaultSubject(string id, string type) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("ID must be provided", nameof(id)); + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); + + defaultSubjectId = id; + defaultSubjectType = type; + + defaultSubjectProperties = new AuthZenPropertyBag(); + return defaultSubjectProperties; + } + + public IAuthZenPropertyBag SetDefaultResource(string id, string type) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("ID must be provided", nameof(id)); + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); + + defaultResourceId = id; + defaultResourceType = type; + + defaultResourceProperties = new AuthZenPropertyBag(); + return defaultResourceProperties; + } + + public IAuthZenPropertyBag SetDefaultAction(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name must be provided", nameof(name)); + + defaultActionName = name; + defaultActionProperties = new AuthZenPropertyBag(); + return defaultActionProperties; + } + + public IAuthZenPropertyBag SetDefaultContext() + { + defaultContextProperties = new AuthZenPropertyBag(); + return defaultContextProperties; + } + + public IAuthZenRequestBuilder AddRequest() + { + var evaluationBuilder = new AuthZenBoxcarEvaluationRequestBuilder(); + + evaluationBuilders.Add(evaluationBuilder); + + return evaluationBuilder; + } + + public IAuthZenBoxcarRequestBuilder SetEvaluationSemantics(BoxcarSemantics semantics) + { + options = new AuthZenBoxcarOptions() + { + Semantics = semantics + }; + + return this; + } + + public AuthZenPayload Build() + { + var payload = new AuthZenBoxcarEvaluationRequest + { + Evaluations = new List(), + DefaultValues = new AuthZenEvaluationRequest(), + }; + + if (!string.IsNullOrWhiteSpace(defaultSubjectId) && !string.IsNullOrWhiteSpace(defaultSubjectType)) + { + payload.DefaultValues.Subject = new AuthZenSubject + { + Id = defaultSubjectId, + Type = defaultSubjectType, + }; + + if (defaultSubjectProperties is { IsEmpty: false }) + { + payload.DefaultValues.Subject.Properties = defaultSubjectProperties.Build(); + } + } + + if (!string.IsNullOrWhiteSpace(defaultResourceId) && !string.IsNullOrWhiteSpace(defaultResourceType)) + { + payload.DefaultValues.Resource = new AuthZenResource + { + Id = defaultResourceId, + Type = defaultResourceType, + }; + + if (defaultResourceProperties is { IsEmpty: false }) + { + payload.DefaultValues.Resource.Properties = defaultResourceProperties.Build(); + } + } + + if (!string.IsNullOrWhiteSpace(defaultActionName)) + { + payload.DefaultValues.Action = new AuthZenAction + { + Name = defaultActionName, + }; + + if (defaultActionProperties is { IsEmpty: false }) + { + payload.DefaultValues.Action.Properties = defaultActionProperties.Build(); + } + } + + if (defaultContextProperties is { IsEmpty: false }) + { + payload.DefaultValues.Context = defaultContextProperties.Build(); + } + + foreach (var builder in evaluationBuilders.Where(eb => eb.HasValuesSet)) + { + var evaluationRequest = builder.Build(); + payload.Evaluations.Add(evaluationRequest); + } + + if (options != null) + { + payload.Options = options; + } + + return new AuthZenPayload + { + CorrelationId = correlationId, + Payload = payload + }; + } + } + + internal class AuthZenBoxcarEvaluationRequestBuilder : IAuthZenRequestBuilder + { + private string subjectId; + private string subjectType; + private string resourceId; + private string resourceType; + private string actionName; + + private AuthZenPropertyBag subjectProperties; + private AuthZenPropertyBag resourceProperties; + private AuthZenPropertyBag actionProperties; + private AuthZenPropertyBag contextProperties; + public bool HasValuesSet => + !string.IsNullOrWhiteSpace(subjectId) || + !string.IsNullOrWhiteSpace(resourceId) || + !string.IsNullOrWhiteSpace(actionName) || + (contextProperties?.IsEmpty == false); + + public IAuthZenPropertyBag SetSubject(string id, string type) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id must be provided", nameof(id)); + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); + + subjectId = id; + subjectType = type; + subjectProperties = new AuthZenPropertyBag(); + return subjectProperties; + } + + public IAuthZenPropertyBag SetResource(string id, string type) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id must be provided", nameof(id)); + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); + + resourceId = id; + resourceType = type; + resourceProperties = new AuthZenPropertyBag(); + return resourceProperties; + } + + public IAuthZenPropertyBag SetAction(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name must be provided", nameof(name)); + + actionName = name; + actionProperties = new AuthZenPropertyBag(); + return actionProperties; + } + + public IAuthZenPropertyBag SetContext() + { + contextProperties = new AuthZenPropertyBag(); + return contextProperties; + } + + public AuthZenEvaluationRequest Build() + { + var request = new AuthZenEvaluationRequest(); + + if (subjectId != null) + { + request.Subject = new AuthZenSubject + { + Id = subjectId, + Type = subjectType, + }; + + if(!subjectProperties.IsEmpty) + { + request.Subject.Properties = subjectProperties.Build(); + } + } + + if (resourceId != null) + { + request.Resource = new AuthZenResource + { + Id = resourceId, + Type = resourceType, + }; + + if (!resourceProperties.IsEmpty) + { + request.Resource.Properties = resourceProperties.Build(); + } + } + + if (actionName != null) + { + request.Action = new AuthZenAction + { + Name = actionName, + }; + + if(!actionProperties.IsEmpty) + { + request.Action.Properties = actionProperties.Build(); + } + } + + if (contextProperties != null && !contextProperties.IsEmpty) + { + request.Context = contextProperties.Build(); + } + + return request; + } + } +} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenClient.cs b/Rsk.AuthZen.Client/IAuthZenClient.cs index 03c8a50..356a1b5 100644 --- a/Rsk.AuthZen.Client/IAuthZenClient.cs +++ b/Rsk.AuthZen.Client/IAuthZenClient.cs @@ -6,7 +6,7 @@ namespace Rsk.AuthZen.Client public interface IAuthZenClient { Task Evaluate( - AuthZenPayload request); + AuthZenPayload request); Task Evaluate( AuthZenPayload request); diff --git a/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs similarity index 83% rename from Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs rename to Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs index fa6df77..dc09fef 100644 --- a/Rsk.AuthZen.Client/IAuthZenRequestBuilder.cs +++ b/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs @@ -8,9 +8,13 @@ public interface IAuthZenRequestBuilder IAuthZenPropertyBag SetResource(string id, string type); IAuthZenPropertyBag SetAction(string name); IAuthZenPropertyBag SetContext(); - IAuthZenRequestBuilder SetCorrelationId(string correlationId); + } + + public interface IAuthZenSingleRequestBuilder : IAuthZenRequestBuilder + { + IAuthZenSingleRequestBuilder SetCorrelationId(string correlationId); - AuthZenPayload Build(); + AuthZenPayload Build(); } public interface IAuthZenPropertyBag From f346ea8e04fa5518582da420fefb6768d87c08a9 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 23 Jun 2025 15:40:22 +0100 Subject: [PATCH 04/33] Remove generic request wrapper --- ...cs => AuthZenBoxcarEvaluationBodyTests.cs} | 48 ++-- .../AuthZenBoxcarRequestBuilderTests.cs | 136 ++++++------ Rsk.AuthZen.Client.Test/AuthZenClientTests.cs | 209 +++++++++--------- .../AuthZenSingleRequestBuilderTests.cs | 56 ++--- ...uest.cs => AuthZenBoxcarEvaluationBody.cs} | 6 +- Rsk.AuthZen.Client/AuthZenClient.cs | 26 +-- ...ionRequest.cs => AuthZenEvaluationBody.cs} | 2 +- Rsk.AuthZen.Client/AuthZenPayload.cs | 8 - .../AuthZenSingleRequestBuilder.cs | 22 +- .../IAuthZenBoxcarRequestBuilder.cs | 42 ++-- Rsk.AuthZen.Client/IAuthZenClient.cs | 6 +- .../IAuthZenSingleRequestBuilder.cs | 8 +- 12 files changed, 284 insertions(+), 285 deletions(-) rename Rsk.AuthZen.Client.Test/{AuthZenBoxcarEvaluationRequestTests.cs => AuthZenBoxcarEvaluationBodyTests.cs} (88%) rename Rsk.AuthZen.Client/{AuthZenBoxcarEvaluationRequest.cs => AuthZenBoxcarEvaluationBody.cs} (93%) rename Rsk.AuthZen.Client/{AuthZenEvaluationRequest.cs => AuthZenEvaluationBody.cs} (97%) delete mode 100644 Rsk.AuthZen.Client/AuthZenPayload.cs diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs b/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs similarity index 88% rename from Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs rename to Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs index 5c8e434..0becde5 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationRequestTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs @@ -4,12 +4,12 @@ namespace Rsk.AuthZen.Client.Test; -public class AuthZenBoxcarEvaluationRequestTests +public class AuthZenBoxcarEvaluationBodyTests { [Fact] public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() { - var defaults = new AuthZenEvaluationRequest() + var defaults = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -20,7 +20,7 @@ public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() }; - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { DefaultValues = defaults }; @@ -37,7 +37,7 @@ public void ToDto_WhenDefaultSubjectIsSet_ShouldPopulateSubject() [Fact] public void ToDto_WhenDefaultResourceIsSet_ShouldPopulateResource() { - var defaults = new AuthZenEvaluationRequest() + var defaults = new AuthZenEvaluationBody() { Resource = new AuthZenResource { @@ -47,7 +47,7 @@ public void ToDto_WhenDefaultResourceIsSet_ShouldPopulateResource() } }; - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { DefaultValues = defaults }; @@ -64,7 +64,7 @@ public void ToDto_WhenDefaultResourceIsSet_ShouldPopulateResource() [Fact] public void ToDto_WhenDefaultActionIsSet_ShouldPopulateAction() { - var defaults = new AuthZenEvaluationRequest() + var defaults = new AuthZenEvaluationBody() { Action = new AuthZenAction { @@ -73,7 +73,7 @@ public void ToDto_WhenDefaultActionIsSet_ShouldPopulateAction() } }; - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { DefaultValues = defaults }; @@ -89,7 +89,7 @@ public void ToDto_WhenDefaultActionIsSet_ShouldPopulateAction() [Fact] public void ToDto_WhenDefaultContextIsSet_ShouldPopulateContext() { - var defaults = new AuthZenEvaluationRequest() + var defaults = new AuthZenEvaluationBody() { Context = new Dictionary { @@ -97,7 +97,7 @@ public void ToDto_WhenDefaultContextIsSet_ShouldPopulateContext() } }; - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { DefaultValues = defaults }; @@ -112,7 +112,7 @@ public void ToDto_WhenDefaultContextIsSet_ShouldPopulateContext() [Fact] public void ToDto_WhenEvaluationsIsMissing_ShouldNotPopulateEvaluations() { - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { Evaluations = null }; @@ -125,9 +125,9 @@ public void ToDto_WhenEvaluationsIsMissing_ShouldNotPopulateEvaluations() [Fact] public void ToDto_WhenEvaluationsIsEmpty_ShouldNotPopulateEvaluations() { - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List() }; var dto = request.ToDto(); @@ -138,9 +138,9 @@ public void ToDto_WhenEvaluationsIsEmpty_ShouldNotPopulateEvaluations() [Fact] public void ToDto_WhenEvaluationsIsSet_ShouldPopulateEachEvaluation() { - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { - Evaluations = new List + Evaluations = new List { new () { @@ -192,9 +192,9 @@ public void ToDto_WhenEvaluationsIsSet_ShouldPopulateEachEvaluation() [Fact] public void ToDto_WhenEvaluationSubjectIsSet_ShouldPopulateSubject() { - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List() { new () { @@ -220,9 +220,9 @@ public void ToDto_WhenEvaluationSubjectIsSet_ShouldPopulateSubject() [Fact] public void ToDto_WhenEvaluationResourceIsSet_ShouldPopulateResource() { - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List() { new () { @@ -249,9 +249,9 @@ public void ToDto_WhenEvaluationResourceIsSet_ShouldPopulateResource() [Fact] public void ToDto_WhenEvaluationActionIsSet_ShouldPopulateAction() { - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List() { new () { @@ -275,9 +275,9 @@ public void ToDto_WhenEvaluationActionIsSet_ShouldPopulateAction() [Fact] public void ToDto_WhenEvaluationContextIsSet_ShouldPopulateContext() { - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List() { new () { @@ -307,9 +307,9 @@ public void ToDto_OptionsAreProvided_ShouldIncludeOptionsInRequestDto(BoxcarSema Semantics = semantics }; - var request = new AuthZenBoxcarEvaluationRequest + var request = new AuthZenBoxcarEvaluationBody { - Evaluations = new List + Evaluations = new List { new () { diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs b/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs index 2b0ecd9..f955eeb 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs @@ -171,12 +171,12 @@ public void Build_WhenCalledWithAllDefaults_ShouldCreateCorrectRequest() request.Should().NotBeNull(); request.CorrelationId.Should().Be("test-correlation-id"); - request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); - request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); - request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); - request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); - request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); - request.Payload.DefaultValues.Context.Should().NotBeNull(); + request.Body.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Body.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Body.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Body.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Body.DefaultValues.Action.Name.Should().Be("action-name"); + request.Body.DefaultValues.Context.Should().NotBeNull(); } [Fact] @@ -194,11 +194,11 @@ public void Build_WhenCalledWithMissingDefaultSubject_ShouldBuildCorrectRequest( request.Should().NotBeNull(); request.CorrelationId.Should().Be("test-correlation-id"); - request.Payload.DefaultValues.Subject.Should().BeNull(); - request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); - request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); - request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); - request.Payload.DefaultValues.Context.Should().NotBeNull(); + request.Body.DefaultValues.Subject.Should().BeNull(); + request.Body.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Body.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Body.DefaultValues.Action.Name.Should().Be("action-name"); + request.Body.DefaultValues.Context.Should().NotBeNull(); } [Fact] @@ -216,11 +216,11 @@ public void Build_WhenCalledWithMissingDefaultResource_ShouldBuildCorrectRequest request.Should().NotBeNull(); request.CorrelationId.Should().Be("test-correlation-id"); - request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); - request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); - request.Payload.DefaultValues.Resource.Should().BeNull(); - request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); - request.Payload.DefaultValues.Context.Should().NotBeNull(); + request.Body.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Body.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Body.DefaultValues.Resource.Should().BeNull(); + request.Body.DefaultValues.Action.Name.Should().Be("action-name"); + request.Body.DefaultValues.Context.Should().NotBeNull(); } [Fact] @@ -238,12 +238,12 @@ public void Build_WhenCalledWithMissingDefaultAction_ShouldBuildCorrectRequest() request.Should().NotBeNull(); request.CorrelationId.Should().Be("test-correlation-id"); - request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); - request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); - request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); - request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); - request.Payload.DefaultValues.Action.Should().BeNull(); - request.Payload.DefaultValues.Context.Should().NotBeNull(); + request.Body.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Body.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Body.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Body.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Body.DefaultValues.Action.Should().BeNull(); + request.Body.DefaultValues.Context.Should().NotBeNull(); } [Fact] @@ -260,12 +260,12 @@ public void Build_WhenCalledWithMissingDefaultContext_ShouldBuildCorrectRequest( request.Should().NotBeNull(); request.CorrelationId.Should().Be("test-correlation-id"); - request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); - request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); - request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); - request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); - request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); - request.Payload.DefaultValues.Context.Should().BeNull(); + request.Body.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Body.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Body.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Body.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Body.DefaultValues.Action.Name.Should().Be("action-name"); + request.Body.DefaultValues.Context.Should().BeNull(); } [Fact] public void Build_WhenCalledWithEmptyDefaultContext_ShouldBuildCorrectRequest() @@ -282,12 +282,12 @@ public void Build_WhenCalledWithEmptyDefaultContext_ShouldBuildCorrectRequest() request.Should().NotBeNull(); request.CorrelationId.Should().Be("test-correlation-id"); - request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); - request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); - request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); - request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); - request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); - request.Payload.DefaultValues.Context.Should().BeNull(); + request.Body.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Body.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Body.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Body.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Body.DefaultValues.Action.Name.Should().Be("action-name"); + request.Body.DefaultValues.Context.Should().BeNull(); } [Fact] @@ -462,27 +462,27 @@ public void AddRequestThenBuild_WhenCalled_ShouldConstructRequestCorrectly() result.Should().NotBeNull(); - result.Payload.Evaluations.Single().Subject.Id.Should().Be(subjectId); - result.Payload.Evaluations.Single().Subject.Type.Should().Be(subjectType); - result.Payload.Evaluations.Single().Subject.Properties.Should().HaveCount(2); - result.Payload.Evaluations.Single().Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty1 && kv.Value.Equals(subjectProperty1Value)); - result.Payload.Evaluations.Single().Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty2 && kv.Value.Equals(subjectProperty2Value)); + result.Body.Evaluations.Single().Subject.Id.Should().Be(subjectId); + result.Body.Evaluations.Single().Subject.Type.Should().Be(subjectType); + result.Body.Evaluations.Single().Subject.Properties.Should().HaveCount(2); + result.Body.Evaluations.Single().Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty1 && kv.Value.Equals(subjectProperty1Value)); + result.Body.Evaluations.Single().Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty2 && kv.Value.Equals(subjectProperty2Value)); - result.Payload.Evaluations.Single().Resource.Id.Should().Be(resourceId); - result.Payload.Evaluations.Single().Resource.Type.Should().Be(resourceType); - result.Payload.Evaluations.Single().Resource.Properties.Should().HaveCount(2); - result.Payload.Evaluations.Single().Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty1 && kv.Value.Equals(resourceProperty1Value)); - result.Payload.Evaluations.Single().Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty2 && kv.Value.Equals(resourceProperty2Value)); + result.Body.Evaluations.Single().Resource.Id.Should().Be(resourceId); + result.Body.Evaluations.Single().Resource.Type.Should().Be(resourceType); + result.Body.Evaluations.Single().Resource.Properties.Should().HaveCount(2); + result.Body.Evaluations.Single().Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty1 && kv.Value.Equals(resourceProperty1Value)); + result.Body.Evaluations.Single().Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty2 && kv.Value.Equals(resourceProperty2Value)); - result.Payload.Evaluations.Single().Action.Name.Should().Be(actionName); - result.Payload.Evaluations.Single().Action.Properties.Should().HaveCount(2); - result.Payload.Evaluations.Single().Action.Properties.Should().Contain(kv => kv.Key == actionProperty1 && kv.Value.Equals(actionProperty1Value)); - result.Payload.Evaluations.Single().Action.Properties.Should().Contain(kv => kv.Key == actionProperty2 && kv.Value.Equals(actionProperty2Value)); + result.Body.Evaluations.Single().Action.Name.Should().Be(actionName); + result.Body.Evaluations.Single().Action.Properties.Should().HaveCount(2); + result.Body.Evaluations.Single().Action.Properties.Should().Contain(kv => kv.Key == actionProperty1 && kv.Value.Equals(actionProperty1Value)); + result.Body.Evaluations.Single().Action.Properties.Should().Contain(kv => kv.Key == actionProperty2 && kv.Value.Equals(actionProperty2Value)); - result.Payload.Evaluations.Single().Context.Should().HaveCount(3); - result.Payload.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty1 && kv.Value.Equals(contextProperty1Value)); - result.Payload.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty2 && kv.Value.Equals(contextProperty2Value)); - result.Payload.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty3 && kv.Value.Equals(contextProperty3Value)); + result.Body.Evaluations.Single().Context.Should().HaveCount(3); + result.Body.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty1 && kv.Value.Equals(contextProperty1Value)); + result.Body.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty2 && kv.Value.Equals(contextProperty2Value)); + result.Body.Evaluations.Single().Context.Should().Contain(kv => kv.Key == contextProperty3 && kv.Value.Equals(contextProperty3Value)); } [Fact] @@ -503,7 +503,7 @@ public void AddRequestThenBuild_WhenCalledWithNoSubject_ShouldNotSetSubject() var result = sut.Build(); - result.Payload.Evaluations.Single().Subject.Should().BeNull(); + result.Body.Evaluations.Single().Subject.Should().BeNull(); } [Fact] @@ -524,7 +524,7 @@ public void AddRequestThenBuild_WhenCalledWithNoResource_ShouldNotSetResource() var result = sut.Build(); - result.Payload.Evaluations.Single().Resource.Should().BeNull(); + result.Body.Evaluations.Single().Resource.Should().BeNull(); } [Fact] @@ -545,7 +545,7 @@ public void AddRequestThenBuild_WhenCalledWithNoAction_ShouldNotSetAction() var result = sut.Build(); - result.Payload.Evaluations.Single().Action.Should().BeNull(); + result.Body.Evaluations.Single().Action.Should().BeNull(); } [Fact] @@ -566,7 +566,7 @@ public void AddRequestThenBuild_WhenCalledWithNoContext_ShouldNotSetContext() var result = sut.Build(); - result.Payload.Evaluations.Single().Context.Should().BeNull(); + result.Body.Evaluations.Single().Context.Should().BeNull(); } [Fact] @@ -578,7 +578,7 @@ public void AddRequestThenBuild_WhenCalledWithSubjectWithNoProperties_ShouldNotS var result = sut.Build(); - result.Payload.Evaluations.Single().Subject.Properties.Should().BeNull(); + result.Body.Evaluations.Single().Subject.Properties.Should().BeNull(); } [Fact] @@ -590,7 +590,7 @@ public void AddRequestThenBuild_WhenCalledWithResourceWithNoProperties_ShouldNot var result = sut.Build(); - result.Payload.Evaluations.Single().Resource.Properties.Should().BeNull(); + result.Body.Evaluations.Single().Resource.Properties.Should().BeNull(); } [Fact] @@ -602,7 +602,7 @@ public void AddRequestThenBuild_WhenCalledWithActionWithNoProperties_ShouldNotSe var result = sut.Build(); - result.Payload.Evaluations.Single().Action.Properties.Should().BeNull(); + result.Body.Evaluations.Single().Action.Properties.Should().BeNull(); } [Fact] @@ -614,7 +614,7 @@ public void AddRequestThenBuild_WhenCalledWithContextWithNoProperties_ShouldNotS var result = sut.Build(); - result.Payload.Evaluations.Should().BeEmpty(); + result.Body.Evaluations.Should().BeEmpty(); } [Theory] @@ -650,13 +650,13 @@ public void Build_WhenSemanticsIsSet_ShouldShouldIncludeSemantics(BoxcarSemantic request.Should().NotBeNull(); request.CorrelationId.Should().Be("test-correlation-id"); - request.Payload.DefaultValues.Subject.Id.Should().Be("subject-id"); - request.Payload.DefaultValues.Subject.Type.Should().Be("subject-type"); - request.Payload.DefaultValues.Resource.Id.Should().Be("resource-id"); - request.Payload.DefaultValues.Resource.Type.Should().Be("resource-type"); - request.Payload.DefaultValues.Action.Name.Should().Be("action-name"); - request.Payload.DefaultValues.Context.Should().NotBeNull(); - request.Payload.Options.Semantics.Should().Be(semantics); + request.Body.DefaultValues.Subject.Id.Should().Be("subject-id"); + request.Body.DefaultValues.Subject.Type.Should().Be("subject-type"); + request.Body.DefaultValues.Resource.Id.Should().Be("resource-id"); + request.Body.DefaultValues.Resource.Type.Should().Be("resource-type"); + request.Body.DefaultValues.Action.Name.Should().Be("action-name"); + request.Body.DefaultValues.Context.Should().NotBeNull(); + request.Body.Options.Semantics.Should().Be(semantics); } [Fact] @@ -673,6 +673,6 @@ public void Build_WhenBoxcarSemanticsIsNotSet_ShouldExcludeFromRequest() var request = sut.Build(); - request.Payload.Options.Should().BeNull(); + request.Body.Options.Should().BeNull(); } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs index 16831ea..9eb9af8 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs @@ -60,7 +60,7 @@ private AuthZenClient CreateSut() return new AuthZenClient(httpClientFactory?.Object, options?.Object); } - private async Task VerifyMissingRequestPartOmitsElement(AuthZenPayload singleEvaluationRequest, string expectedMissingElement) + private async Task VerifyMissingRequestPartOmitsElement(AuthZenEvaluationRequest singleEvaluationRequest, string expectedMissingElement) { HttpRequestMessage requestSent = null; httpMessageHandler.Protected() @@ -182,9 +182,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostToCorrectEnd var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -216,9 +216,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationWithCorrelationId_Shoul var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest + Body = new AuthZenEvaluationBody { Subject = new AuthZenSubject { @@ -249,9 +249,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRe var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -299,15 +299,15 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRe AdjustRequestSerialization(deserializedRequest); - deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Payload.ToDto()); + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Body.ToDto()); } [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoSubject_ShouldNotSerializeSubject() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Resource = new AuthZenResource { @@ -342,9 +342,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoSubject_ShouldNotS [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoResource_ShouldNotSerializeResource() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -379,10 +379,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoResource_ShouldNot [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoAction_ShouldNotSerializeAction() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() - + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -417,9 +416,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoAction_ShouldNotSe [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndNoContext_ShouldNotSerializeContext() { - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -474,9 +473,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldParseDecisionCor var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -521,9 +520,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldExtractContextCo var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -548,9 +547,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndRequestFails_ShouldT var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -585,9 +584,9 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndResponseContainsRequ var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenEvaluationRequest { - Payload = new AuthZenEvaluationRequest() + Body = new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -616,11 +615,11 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List + Evaluations = new List { new() { @@ -631,7 +630,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() } }; @@ -658,12 +657,12 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrel var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { CorrelationId = Guid.NewGuid().ToString(), - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List + Evaluations = new List { new() { @@ -674,7 +673,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrel } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() }, }; @@ -700,12 +699,11 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - CorrelationId = Guid.NewGuid().ToString(), - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { new() { @@ -716,8 +714,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos } } }, - DefaultValues = new AuthZenEvaluationRequest() - } + DefaultValues = new AuthZenEvaluationBody() + }, + CorrelationId = Guid.NewGuid().ToString(), }; await sut.Evaluate(evaluationRequest); @@ -729,7 +728,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos AdjustBoxcarRequestSerialization(deserializedRequest); - deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Payload.ToDto()); + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Body.ToDto()); } [Theory] @@ -759,13 +758,13 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPar var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -830,13 +829,13 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldExt var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest + Body = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -863,13 +862,13 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRequest var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest + Body = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -910,15 +909,15 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRespons var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new AuthZenEvaluationBody() { Subject = new AuthZenSubject { @@ -949,11 +948,11 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List + Evaluations = new List { new() { @@ -964,7 +963,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1006,14 +1005,11 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - CorrelationId = Guid.NewGuid().ToString(), - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - - - Evaluations = new List + Evaluations = new List { new() { @@ -1024,7 +1020,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1041,7 +1037,8 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit Name = "hjkldfgb" } } - } + }, + CorrelationId = Guid.NewGuid().ToString(), }; await sut.Evaluate(evaluationRequest); @@ -1066,12 +1063,11 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - CorrelationId = Guid.NewGuid().ToString(), - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { new() { @@ -1082,7 +1078,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1099,7 +1095,8 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh Name = "hjkldfgb" } } - } + }, + CorrelationId = Guid.NewGuid().ToString(), }; await sut.Evaluate(evaluationRequest); @@ -1111,7 +1108,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh AdjustBoxcarRequestSerialization(deserializedRequest); - deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Payload.ToDto()); + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Body.ToDto()); } [Theory] @@ -1141,13 +1138,13 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new() { Subject = new AuthZenSubject { @@ -1156,7 +1153,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1229,13 +1226,13 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new() { Subject = new AuthZenSubject { @@ -1244,7 +1241,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1280,13 +1277,13 @@ public async Task var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new() { Subject = new AuthZenSubject { @@ -1295,7 +1292,7 @@ public async Task } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1344,13 +1341,13 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { - new AuthZenEvaluationRequest() + new() { Subject = new AuthZenSubject { @@ -1359,7 +1356,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1401,12 +1398,11 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - CorrelationId = Guid.NewGuid().ToString(), - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List() { new() { @@ -1417,7 +1413,7 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS } } }, - DefaultValues = new AuthZenEvaluationRequest() + DefaultValues = new AuthZenEvaluationBody() { Subject = new AuthZenSubject() { @@ -1438,7 +1434,8 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS { Semantics = semantics } - } + }, + CorrelationId = Guid.NewGuid().ToString(), }; await sut.Evaluate(evaluationRequest); @@ -1450,7 +1447,7 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS AdjustBoxcarRequestSerialization(deserializedRequest); - var expectation = evaluationRequest.Payload.ToDto(); + var expectation = evaluationRequest.Body.ToDto(); deserializedRequest.Should().BeEquivalentTo(expectation); } @@ -1468,12 +1465,12 @@ public async Task Evaluate_WhenBoxCarEvaluationsIsMissing_ShouldFallbackToSingle var sut = CreateSut(); - var evaluationRequest = new AuthZenPayload() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - Payload = new AuthZenBoxcarEvaluationRequest() + Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List(), - DefaultValues = new AuthZenEvaluationRequest + Evaluations = new List(), + DefaultValues = new AuthZenEvaluationBody { Subject = new AuthZenSubject { diff --git a/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs b/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs index 12f082f..0f5891e 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs @@ -165,27 +165,27 @@ public void Build_WhenCalled_ShouldConstructRequestCorrectly() result.Should().NotBeNull(); - result.Payload.Subject.Id.Should().Be(subjectId); - result.Payload.Subject.Type.Should().Be(subjectType); - result.Payload.Subject.Properties.Should().HaveCount(2); - result.Payload.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty1 && kv.Value.Equals(subjectProperty1Value)); - result.Payload.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty2 && kv.Value.Equals(subjectProperty2Value)); - - result.Payload.Resource.Id.Should().Be(resourceId); - result.Payload.Resource.Type.Should().Be(resourceType); - result.Payload.Resource.Properties.Should().HaveCount(2); - result.Payload.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty1 && kv.Value.Equals(resourceProperty1Value)); - result.Payload.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty2 && kv.Value.Equals(resourceProperty2Value)); - - result.Payload.Action.Name.Should().Be(actionName); - result.Payload.Action.Properties.Should().HaveCount(2); - result.Payload.Action.Properties.Should().Contain(kv => kv.Key == actionProperty1 && kv.Value.Equals(actionProperty1Value)); - result.Payload.Action.Properties.Should().Contain(kv => kv.Key == actionProperty2 && kv.Value.Equals(actionProperty2Value)); + result.Body.Subject.Id.Should().Be(subjectId); + result.Body.Subject.Type.Should().Be(subjectType); + result.Body.Subject.Properties.Should().HaveCount(2); + result.Body.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty1 && kv.Value.Equals(subjectProperty1Value)); + result.Body.Subject.Properties.Should().Contain(kv => kv.Key == subjectProperty2 && kv.Value.Equals(subjectProperty2Value)); + + result.Body.Resource.Id.Should().Be(resourceId); + result.Body.Resource.Type.Should().Be(resourceType); + result.Body.Resource.Properties.Should().HaveCount(2); + result.Body.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty1 && kv.Value.Equals(resourceProperty1Value)); + result.Body.Resource.Properties.Should().Contain(kv => kv.Key == resourceProperty2 && kv.Value.Equals(resourceProperty2Value)); + + result.Body.Action.Name.Should().Be(actionName); + result.Body.Action.Properties.Should().HaveCount(2); + result.Body.Action.Properties.Should().Contain(kv => kv.Key == actionProperty1 && kv.Value.Equals(actionProperty1Value)); + result.Body.Action.Properties.Should().Contain(kv => kv.Key == actionProperty2 && kv.Value.Equals(actionProperty2Value)); - result.Payload.Context.Should().HaveCount(3); - result.Payload.Context.Should().Contain(kv => kv.Key == contextProperty1 && kv.Value.Equals(contextProperty1Value)); - result.Payload.Context.Should().Contain(kv => kv.Key == contextProperty2 && kv.Value.Equals(contextProperty2Value)); - result.Payload.Context.Should().Contain(kv => kv.Key == contextProperty3 && kv.Value.Equals(contextProperty3Value)); + result.Body.Context.Should().HaveCount(3); + result.Body.Context.Should().Contain(kv => kv.Key == contextProperty1 && kv.Value.Equals(contextProperty1Value)); + result.Body.Context.Should().Contain(kv => kv.Key == contextProperty2 && kv.Value.Equals(contextProperty2Value)); + result.Body.Context.Should().Contain(kv => kv.Key == contextProperty3 && kv.Value.Equals(contextProperty3Value)); } [Fact] @@ -204,7 +204,7 @@ public void Build_WhenCalledWithNoSubject_ShouldNotSetSubject() var result = sut.Build(); - result.Payload.Subject.Should().BeNull(); + result.Body.Subject.Should().BeNull(); } [Fact] @@ -223,7 +223,7 @@ public void Build_WhenCalledWithNoResource_ShouldNotSetResource() var result = sut.Build(); - result.Payload.Resource.Should().BeNull(); + result.Body.Resource.Should().BeNull(); } [Fact] @@ -242,7 +242,7 @@ public void Build_WhenCalledWithNoAction_ShouldNotSetAction() var result = sut.Build(); - result.Payload.Action.Should().BeNull(); + result.Body.Action.Should().BeNull(); } [Fact] @@ -261,7 +261,7 @@ public void Build_WhenCalledWithNoContext_ShouldNotSetContext() var result = sut.Build(); - result.Payload.Context.Should().BeNull(); + result.Body.Context.Should().BeNull(); } [Fact] @@ -273,7 +273,7 @@ public void Build_WhenCalledWithSubjectWithNoProperties_ShouldNotSetSubjectPrope var result = sut.Build(); - result.Payload.Subject.Properties.Should().BeNull(); + result.Body.Subject.Properties.Should().BeNull(); } [Fact] @@ -285,7 +285,7 @@ public void Build_WhenCalledWithResourceWithNoProperties_ShouldNotSetResourcePro var result = sut.Build(); - result.Payload.Resource.Properties.Should().BeNull(); + result.Body.Resource.Properties.Should().BeNull(); } [Fact] @@ -297,7 +297,7 @@ public void Build_WhenCalledWithActionWithNoProperties_ShouldNotSetActionPropert var result = sut.Build(); - result.Payload.Action.Properties.Should().BeNull(); + result.Body.Action.Properties.Should().BeNull(); } [Fact] @@ -309,7 +309,7 @@ public void Build_WhenCalledWithContextWithNoProperties_ShouldNotSetContext() var result = sut.Build(); - result.Payload.Context.Should().BeNull(); + result.Body.Context.Should().BeNull(); } [Fact] diff --git a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs similarity index 93% rename from Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs rename to Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs index 3e6b502..bcc2471 100644 --- a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationRequest.cs +++ b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs @@ -4,10 +4,10 @@ namespace Rsk.AuthZen.Client { - public class AuthZenBoxcarEvaluationRequest + public class AuthZenBoxcarEvaluationBody { - public List Evaluations { get; internal set; } - public AuthZenEvaluationRequest DefaultValues { get; internal set; } + public List Evaluations { get; internal set; } + public AuthZenEvaluationBody DefaultValues { get; internal set; } public AuthZenBoxcarOptions Options { get; internal set; } internal AuthZenBoxcarRequestMessageDto ToDto() diff --git a/Rsk.AuthZen.Client/AuthZenClient.cs b/Rsk.AuthZen.Client/AuthZenClient.cs index 974001e..a9239c0 100644 --- a/Rsk.AuthZen.Client/AuthZenClient.cs +++ b/Rsk.AuthZen.Client/AuthZenClient.cs @@ -42,7 +42,7 @@ public AuthZenClient(IHttpClientFactory httpClientFactory, IOptions Evaluate(AuthZenPayload request) + public async Task Evaluate(AuthZenEvaluationRequest request) { var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{EvaluationUri}", UriKind.Relative)); if (request.CorrelationId != null) @@ -50,7 +50,7 @@ public async Task Evaluate(AuthZenPayload Evaluate(AuthZenPayload Evaluate(AuthZenPayload request) + public async Task Evaluate(AuthZenBoxcarEvaluationRequest request) { if (IsMultiEvaluationsMissing(request)) { @@ -93,7 +93,7 @@ public async Task Evaluate(AuthZenPayload Evaluate(AuthZenPayload evaluationRequest) + private static bool IsMultiEvaluationsMissing(AuthZenBoxcarEvaluationRequest evaluationRequest) { - return evaluationRequest.Payload.Evaluations == null || !evaluationRequest.Payload.Evaluations.Any(); + return evaluationRequest.Body.Evaluations == null || !evaluationRequest.Body.Evaluations.Any(); } - private async Task FallbackToSingleEvaluation(AuthZenPayload evaluationRequest) + private async Task FallbackToSingleEvaluation(AuthZenBoxcarEvaluationRequest evaluationRequest) { - var singleResponse = await Evaluate(new AuthZenPayload() + var singleResponse = await Evaluate(new AuthZenEvaluationRequest { CorrelationId = evaluationRequest.CorrelationId, - Payload = new AuthZenEvaluationRequest + Body = new AuthZenEvaluationBody { - Context = evaluationRequest.Payload.DefaultValues.Context, - Subject = evaluationRequest.Payload.DefaultValues.Subject, - Resource = evaluationRequest.Payload.DefaultValues.Resource, - Action = evaluationRequest.Payload.DefaultValues.Action, + Context = evaluationRequest.Body.DefaultValues.Context, + Subject = evaluationRequest.Body.DefaultValues.Subject, + Resource = evaluationRequest.Body.DefaultValues.Resource, + Action = evaluationRequest.Body.DefaultValues.Action, } }); diff --git a/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs b/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs similarity index 97% rename from Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs rename to Rsk.AuthZen.Client/AuthZenEvaluationBody.cs index ae635ac..d1332f0 100644 --- a/Rsk.AuthZen.Client/AuthZenEvaluationRequest.cs +++ b/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs @@ -3,7 +3,7 @@ namespace Rsk.AuthZen.Client { - public class AuthZenEvaluationRequest + public class AuthZenEvaluationBody { public AuthZenSubject Subject { get; internal set; } public AuthZenResource Resource { get; internal set; } diff --git a/Rsk.AuthZen.Client/AuthZenPayload.cs b/Rsk.AuthZen.Client/AuthZenPayload.cs deleted file mode 100644 index 0dd8754..0000000 --- a/Rsk.AuthZen.Client/AuthZenPayload.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Rsk.AuthZen.Client -{ - public class AuthZenPayload - { - public string CorrelationId { get; internal set; } - public T Payload { get; internal set; } - } -} \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs b/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs index 1e0d369..c169e93 100644 --- a/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs +++ b/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs @@ -63,13 +63,13 @@ public IAuthZenPropertyBag SetContext() return contextProperties; } - public AuthZenPayload Build() + public AuthZenEvaluationRequest Build() { - var request = new AuthZenEvaluationRequest(); + var body = new AuthZenEvaluationBody(); if (subjectId != null) { - request.Subject = new AuthZenSubject + body.Subject = new AuthZenSubject { Id = subjectId, Type = subjectType, @@ -77,13 +77,13 @@ public AuthZenPayload Build() if(!subjectProperties.IsEmpty) { - request.Subject.Properties = subjectProperties.Build(); + body.Subject.Properties = subjectProperties.Build(); } } if (resourceId != null) { - request.Resource = new AuthZenResource + body.Resource = new AuthZenResource { Id = resourceId, Type = resourceType, @@ -91,31 +91,31 @@ public AuthZenPayload Build() if (!resourceProperties.IsEmpty) { - request.Resource.Properties = resourceProperties.Build(); + body.Resource.Properties = resourceProperties.Build(); } } if (actionName != null) { - request.Action = new AuthZenAction + body.Action = new AuthZenAction { Name = actionName, }; if(!actionProperties.IsEmpty) { - request.Action.Properties = actionProperties.Build(); + body.Action.Properties = actionProperties.Build(); } } if (contextProperties != null && !contextProperties.IsEmpty) { - request.Context = contextProperties.Build(); + body.Context = contextProperties.Build(); } - return new AuthZenPayload + return new AuthZenEvaluationRequest { - Payload = request, + Body = body, CorrelationId = correlationId }; } diff --git a/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs index d3422f2..5b1a240 100644 --- a/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs +++ b/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs @@ -15,7 +15,13 @@ public interface IAuthZenBoxcarRequestBuilder IAuthZenRequestBuilder AddRequest(); - AuthZenPayload Build(); + AuthZenBoxcarEvaluationRequest Build(); + } + + public class AuthZenBoxcarEvaluationRequest + { + public string CorrelationId { get; internal set; } + public AuthZenBoxcarEvaluationBody Body { get; internal set; } } public class AuthZenBoxcarRequestBuilder : IAuthZenBoxcarRequestBuilder @@ -103,17 +109,17 @@ public IAuthZenBoxcarRequestBuilder SetEvaluationSemantics(BoxcarSemantics seman return this; } - public AuthZenPayload Build() + public AuthZenBoxcarEvaluationRequest Build() { - var payload = new AuthZenBoxcarEvaluationRequest + var body = new AuthZenBoxcarEvaluationBody { - Evaluations = new List(), - DefaultValues = new AuthZenEvaluationRequest(), + Evaluations = new List(), + DefaultValues = new AuthZenEvaluationBody(), }; if (!string.IsNullOrWhiteSpace(defaultSubjectId) && !string.IsNullOrWhiteSpace(defaultSubjectType)) { - payload.DefaultValues.Subject = new AuthZenSubject + body.DefaultValues.Subject = new AuthZenSubject { Id = defaultSubjectId, Type = defaultSubjectType, @@ -121,13 +127,13 @@ public AuthZenPayload Build() if (defaultSubjectProperties is { IsEmpty: false }) { - payload.DefaultValues.Subject.Properties = defaultSubjectProperties.Build(); + body.DefaultValues.Subject.Properties = defaultSubjectProperties.Build(); } } if (!string.IsNullOrWhiteSpace(defaultResourceId) && !string.IsNullOrWhiteSpace(defaultResourceType)) { - payload.DefaultValues.Resource = new AuthZenResource + body.DefaultValues.Resource = new AuthZenResource { Id = defaultResourceId, Type = defaultResourceType, @@ -135,43 +141,43 @@ public AuthZenPayload Build() if (defaultResourceProperties is { IsEmpty: false }) { - payload.DefaultValues.Resource.Properties = defaultResourceProperties.Build(); + body.DefaultValues.Resource.Properties = defaultResourceProperties.Build(); } } if (!string.IsNullOrWhiteSpace(defaultActionName)) { - payload.DefaultValues.Action = new AuthZenAction + body.DefaultValues.Action = new AuthZenAction { Name = defaultActionName, }; if (defaultActionProperties is { IsEmpty: false }) { - payload.DefaultValues.Action.Properties = defaultActionProperties.Build(); + body.DefaultValues.Action.Properties = defaultActionProperties.Build(); } } if (defaultContextProperties is { IsEmpty: false }) { - payload.DefaultValues.Context = defaultContextProperties.Build(); + body.DefaultValues.Context = defaultContextProperties.Build(); } foreach (var builder in evaluationBuilders.Where(eb => eb.HasValuesSet)) { var evaluationRequest = builder.Build(); - payload.Evaluations.Add(evaluationRequest); + body.Evaluations.Add(evaluationRequest); } if (options != null) { - payload.Options = options; + body.Options = options; } - return new AuthZenPayload + return new AuthZenBoxcarEvaluationRequest { CorrelationId = correlationId, - Payload = payload + Body = body }; } } @@ -231,9 +237,9 @@ public IAuthZenPropertyBag SetContext() return contextProperties; } - public AuthZenEvaluationRequest Build() + public AuthZenEvaluationBody Build() { - var request = new AuthZenEvaluationRequest(); + var request = new AuthZenEvaluationBody(); if (subjectId != null) { diff --git a/Rsk.AuthZen.Client/IAuthZenClient.cs b/Rsk.AuthZen.Client/IAuthZenClient.cs index 356a1b5..629137b 100644 --- a/Rsk.AuthZen.Client/IAuthZenClient.cs +++ b/Rsk.AuthZen.Client/IAuthZenClient.cs @@ -5,10 +5,8 @@ namespace Rsk.AuthZen.Client { public interface IAuthZenClient { - Task Evaluate( - AuthZenPayload request); + Task Evaluate(AuthZenEvaluationRequest request); - Task Evaluate( - AuthZenPayload request); + Task Evaluate(AuthZenBoxcarEvaluationRequest request); } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs index dc09fef..616b393 100644 --- a/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs +++ b/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs @@ -14,7 +14,13 @@ public interface IAuthZenSingleRequestBuilder : IAuthZenRequestBuilder { IAuthZenSingleRequestBuilder SetCorrelationId(string correlationId); - AuthZenPayload Build(); + AuthZenEvaluationRequest Build(); + } + + public class AuthZenEvaluationRequest + { + public string CorrelationId { get; internal set; } + public AuthZenEvaluationBody Body { get; internal set; } } public interface IAuthZenPropertyBag From 611550386b5b2567b104b4a6e5f2be593e996d8e Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 23 Jun 2025 17:21:49 +0100 Subject: [PATCH 05/33] Add documentation --- Rsk.AuthZen.Client/AuthZenAction.cs | 10 + .../AuthZenBoxcarEvaluationBody.cs | 69 +++++- Rsk.AuthZen.Client/AuthZenClient.cs | 16 ++ Rsk.AuthZen.Client/AuthZenEvaluationBody.cs | 30 ++- .../AuthZenRequestFailureException.cs | 17 +- Rsk.AuthZen.Client/AuthZenResource.cs | 14 ++ Rsk.AuthZen.Client/AuthZenResponse.cs | 14 ++ .../AuthZenSingleRequestBuilder.cs | 74 ++++-- Rsk.AuthZen.Client/AuthZenSubject.cs | 14 ++ Rsk.AuthZen.Client/Decision.cs | 10 + .../IAuthZenBoxcarRequestBuilder.cs | 213 ++++++++++++++---- Rsk.AuthZen.Client/IAuthZenClient.cs | 19 +- .../IAuthZenSingleRequestBuilder.cs | 100 +++++++- Rsk.AuthZen.sln.DotSettings.user | 1 + 14 files changed, 513 insertions(+), 88 deletions(-) diff --git a/Rsk.AuthZen.Client/AuthZenAction.cs b/Rsk.AuthZen.Client/AuthZenAction.cs index af6f565..b38ac09 100644 --- a/Rsk.AuthZen.Client/AuthZenAction.cs +++ b/Rsk.AuthZen.Client/AuthZenAction.cs @@ -2,9 +2,19 @@ namespace Rsk.AuthZen.Client { + /// + /// Represents an action in AuthZen, including its name and associated properties. + /// public class AuthZenAction { + /// + /// Gets the name of the action. + /// public string Name { get; internal set; } + + /// + /// Gets the properties associated with the action. + /// public Dictionary Properties { get; internal set; } } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs index bcc2471..07c23bf 100644 --- a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs +++ b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs @@ -4,12 +4,31 @@ namespace Rsk.AuthZen.Client { + /// + /// Represents a request body for a boxcar evaluation in AuthZen, containing multiple evaluations, + /// default values, and evaluation options. + /// public class AuthZenBoxcarEvaluationBody { + /// + /// Gets the list of individual evaluation bodies to be processed in the boxcar request. + /// public List Evaluations { get; internal set; } + + /// + /// Gets the default values to be applied to each evaluation if not explicitly set. + /// public AuthZenEvaluationBody DefaultValues { get; internal set; } + + /// + /// Gets the options that control the semantics of the boxcar evaluation. + /// public AuthZenBoxcarOptions Options { get; internal set; } - + + /// + /// Converts this instance to a for transmission. + /// + /// The corresponding . internal AuthZenBoxcarRequestMessageDto ToDto() { var dto = new AuthZenBoxcarRequestMessageDto(); @@ -53,7 +72,7 @@ internal AuthZenBoxcarRequestMessageDto ToDto() dto.Evaluations[i] = Evaluations[i].ToDto(); } } - + if (Options != null) { dto.Options = Options.ToDto(); @@ -63,26 +82,58 @@ internal AuthZenBoxcarRequestMessageDto ToDto() } } + /// + /// Represents the response from a boxcar evaluation request in AuthZen, + /// including a correlation identifier and the results of individual evaluations. + /// public class AuthZenBoxcarResponse { + /// + /// Gets the correlation identifier for the boxcar evaluation request. + /// public string CorrelationId { get; internal set; } + + /// + /// Gets the list of evaluation results returned by the boxcar request. + /// public List Evaluations { get; internal set; } } + /// + /// Defines the semantics for boxcar evaluations in AuthZen. + /// public enum BoxcarSemantics { + /// + /// Indicates that all evaluations should be executed. + /// ExecuteAll, + + /// + /// Indicates that the evaluation should stop and deny on the first deny result. + /// DenyOnFirstDeny, + + /// + /// Indicates that the evaluation should stop and permit on the first permit result. + /// PermitOnFirstPermit } - // execute_all - // deny_on_first_deny - // permit_on_first_permit + /// + /// Represents the options for a boxcar evaluation in AuthZen, including evaluation semantics. + /// public class AuthZenBoxcarOptions { + /// + /// Gets the semantics that control the evaluation behavior in the boxcar request. + /// public BoxcarSemantics Semantics { get; internal set; } - + + /// + /// Converts this instance to a for transmission. + /// + /// The corresponding . internal AuthZenBoxcarOptionsDto ToDto() { return new AuthZenBoxcarOptionsDto @@ -91,6 +142,12 @@ internal AuthZenBoxcarOptionsDto ToDto() }; } + /// + /// Converts the enumeration value to its string representation. + /// + /// The semantics to convert. + /// The string representation of the semantics. + /// Thrown if the semantics value is not supported. private static string ConvertSemantics(BoxcarSemantics semantics) { return semantics switch diff --git a/Rsk.AuthZen.Client/AuthZenClient.cs b/Rsk.AuthZen.Client/AuthZenClient.cs index a9239c0..154fbf4 100644 --- a/Rsk.AuthZen.Client/AuthZenClient.cs +++ b/Rsk.AuthZen.Client/AuthZenClient.cs @@ -11,11 +11,18 @@ namespace Rsk.AuthZen.Client { + /// + /// Provides configuration options for the . + /// public class AuthZenClientOptions { + /// + /// Gets or sets the base URL of the AuthZen authorization service. + /// public string AuthorizationUrl { get; set; } } + /// public class AuthZenClient : IAuthZenClient { private const string AuthZenContentType = "application/json"; @@ -32,6 +39,13 @@ public class AuthZenClient : IAuthZenClient DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; + /// + /// Creates a new instance of . + /// + /// The httpClientFactory + /// The AuthZenClientOptions + /// Thrown if any argument is null + /// Thrown if the AuthZenClientOptions are invalid public AuthZenClient(IHttpClientFactory httpClientFactory, IOptions options) { if (httpClientFactory == null) throw new ArgumentNullException(nameof(httpClientFactory)); @@ -42,6 +56,7 @@ public AuthZenClient(IHttpClientFactory httpClientFactory, IOptions public async Task Evaluate(AuthZenEvaluationRequest request) { var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{EvaluationUri}", UriKind.Relative)); @@ -79,6 +94,7 @@ public async Task Evaluate(AuthZenEvaluationRequest request) return authZenResponse; } + /// public async Task Evaluate(AuthZenBoxcarEvaluationRequest request) { if (IsMultiEvaluationsMissing(request)) diff --git a/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs b/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs index d1332f0..f814f5d 100644 --- a/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs +++ b/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs @@ -3,17 +3,39 @@ namespace Rsk.AuthZen.Client { + /// + /// Represents the body of an evaluation request in AuthZen, including subject, resource, action, and context. + /// public class AuthZenEvaluationBody { + /// + /// Gets the subject for the evaluation request. + /// public AuthZenSubject Subject { get; internal set; } + + /// + /// Gets the resource for the evaluation request. + /// public AuthZenResource Resource { get; internal set; } + + /// + /// Gets the action to be evaluated. + /// public AuthZenAction Action { get; internal set; } + + /// + /// Gets the context data for the evaluation request. + /// public Dictionary Context { get; internal set; } + /// + /// Converts this instance to a for transmission. + /// + /// The corresponding . internal AuthZenRequestMessageDto ToDto() { var dto = new AuthZenRequestMessageDto(); - + if (Subject != null) { dto.Subject = new AuthZenSubjectDto @@ -23,7 +45,7 @@ internal AuthZenRequestMessageDto ToDto() Properties = Subject.Properties }; } - + if (Resource != null) { dto.Resource = new AuthZenResourceDto @@ -33,7 +55,7 @@ internal AuthZenRequestMessageDto ToDto() Properties = Resource.Properties }; } - + if (Action != null) { dto.Action = new AuthZenActionDto @@ -42,7 +64,7 @@ internal AuthZenRequestMessageDto ToDto() Properties = Action.Properties }; } - + dto.Context = Context; return dto; diff --git a/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs b/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs index b5672e1..40df6c0 100644 --- a/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs +++ b/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs @@ -2,16 +2,31 @@ namespace Rsk.AuthZen.Client { + /// + /// Represents an error that occurs when a request to the AuthZen service fails. + /// public class AuthZenRequestFailureException : Exception { + /// + /// Initializes a new instance of the class. + /// public AuthZenRequestFailureException() { } - + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message public AuthZenRequestFailureException(string message) : base(message) { } + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message + /// The inner exception + /// public AuthZenRequestFailureException(string message, Exception inner) : base(message, inner) { } diff --git a/Rsk.AuthZen.Client/AuthZenResource.cs b/Rsk.AuthZen.Client/AuthZenResource.cs index b09ffec..6d6c5fe 100644 --- a/Rsk.AuthZen.Client/AuthZenResource.cs +++ b/Rsk.AuthZen.Client/AuthZenResource.cs @@ -2,10 +2,24 @@ namespace Rsk.AuthZen.Client { + /// + /// Represents a resource in an AuthZen evaluation request, including its identifier, type, and additional properties. + /// public class AuthZenResource { + /// + /// Gets the unique identifier of the resource. + /// public string Id { get; internal set; } + + /// + /// Gets the type of the resource. + /// public string Type { get; internal set; } + + /// + /// Gets additional properties associated with the resource. + /// public Dictionary Properties { get; internal set; } } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenResponse.cs b/Rsk.AuthZen.Client/AuthZenResponse.cs index 888d0ee..4fa7a75 100644 --- a/Rsk.AuthZen.Client/AuthZenResponse.cs +++ b/Rsk.AuthZen.Client/AuthZenResponse.cs @@ -1,9 +1,23 @@ namespace Rsk.AuthZen.Client { + /// + /// Represents the response from an AuthZen evaluation, including the decision, context, and correlation identifier. + /// public class AuthZenResponse { + /// + /// Gets the decision result of the evaluation. + /// public Decision Decision { get; internal set; } + + /// + /// Gets the context information associated with the evaluation response. + /// public string Context { get; internal set; } + + /// + /// Gets the correlation identifier for tracking the evaluation request and response. + /// public string CorrelationId { get; internal set; } } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs b/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs index c169e93..62782e1 100644 --- a/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs +++ b/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs @@ -2,71 +2,99 @@ namespace Rsk.AuthZen.Client { + /// + /// Provides a fluent builder for constructing single AuthZen evaluation requests, + /// allowing configuration of subject, resource, action, context, and correlation ID. + /// public class AuthZenSingleRequestBuilder : IAuthZenSingleRequestBuilder { private string correlationId; - private string subjectId; private string subjectType; private string resourceId; private string resourceType; private string actionName; - + private AuthZenPropertyBag subjectProperties; private AuthZenPropertyBag resourceProperties; private AuthZenPropertyBag actionProperties; private AuthZenPropertyBag contextProperties; - + + /// + /// Sets the correlation identifier for the evaluation request. + /// + /// The correlation ID to associate with the request. + /// The current builder instance. public IAuthZenSingleRequestBuilder SetCorrelationId(string id) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Correlation ID must be provided", nameof(id)); - correlationId = id; - return this; } - + + /// + /// Sets the subject for the evaluation request. + /// + /// The subject identifier. + /// The subject type. + /// A property bag for adding subject properties. public IAuthZenPropertyBag SetSubject(string id, string type) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id must be provided", nameof(id)); if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); - subjectId = id; subjectType = type; subjectProperties = new AuthZenPropertyBag(); return subjectProperties; } - + + /// + /// Sets the resource for the evaluation request. + /// + /// The resource identifier. + /// The resource type. + /// A property bag for adding resource properties. public IAuthZenPropertyBag SetResource(string id, string type) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id must be provided", nameof(id)); if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); - resourceId = id; resourceType = type; resourceProperties = new AuthZenPropertyBag(); return resourceProperties; } - + + /// + /// Sets the action for the evaluation request. + /// + /// The action name. + /// A property bag for adding action properties. public IAuthZenPropertyBag SetAction(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name must be provided", nameof(name)); - actionName = name; actionProperties = new AuthZenPropertyBag(); return actionProperties; } - + + /// + /// Sets the context for the evaluation request. + /// + /// A property bag for adding context properties. public IAuthZenPropertyBag SetContext() { contextProperties = new AuthZenPropertyBag(); return contextProperties; } - + + /// + /// Builds the instance using the configured values. + /// + /// The constructed . public AuthZenEvaluationRequest Build() { var body = new AuthZenEvaluationBody(); - + if (subjectId != null) { body.Subject = new AuthZenSubject @@ -74,13 +102,13 @@ public AuthZenEvaluationRequest Build() Id = subjectId, Type = subjectType, }; - - if(!subjectProperties.IsEmpty) + + if (!subjectProperties.IsEmpty) { body.Subject.Properties = subjectProperties.Build(); } } - + if (resourceId != null) { body.Resource = new AuthZenResource @@ -88,31 +116,31 @@ public AuthZenEvaluationRequest Build() Id = resourceId, Type = resourceType, }; - + if (!resourceProperties.IsEmpty) { body.Resource.Properties = resourceProperties.Build(); } } - + if (actionName != null) { body.Action = new AuthZenAction { Name = actionName, }; - - if(!actionProperties.IsEmpty) + + if (!actionProperties.IsEmpty) { body.Action.Properties = actionProperties.Build(); } } - + if (contextProperties != null && !contextProperties.IsEmpty) { body.Context = contextProperties.Build(); } - + return new AuthZenEvaluationRequest { Body = body, diff --git a/Rsk.AuthZen.Client/AuthZenSubject.cs b/Rsk.AuthZen.Client/AuthZenSubject.cs index dc16fcc..b60e8ee 100644 --- a/Rsk.AuthZen.Client/AuthZenSubject.cs +++ b/Rsk.AuthZen.Client/AuthZenSubject.cs @@ -2,10 +2,24 @@ namespace Rsk.AuthZen.Client { + /// + /// Represents a subject in an AuthZen evaluation request, including its identifier, type, and additional properties. + /// public class AuthZenSubject { + /// + /// Gets the unique identifier of the subject. + /// public string Id { get; internal set; } + + /// + /// Gets the type of the subject. + /// public string Type { get; internal set; } + + /// + /// Gets additional properties associated with the subject. + /// public Dictionary Properties { get; internal set; } } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/Decision.cs b/Rsk.AuthZen.Client/Decision.cs index bc33886..64b9038 100644 --- a/Rsk.AuthZen.Client/Decision.cs +++ b/Rsk.AuthZen.Client/Decision.cs @@ -1,8 +1,18 @@ namespace Rsk.AuthZen.Client { + /// + /// Specifies the possible decisions returned by an AuthZen evaluation. + /// public enum Decision { + /// + /// The request is permitted. + /// Permit, + + /// + /// The request is denied. + /// Deny } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs index 5b1a240..342a816 100644 --- a/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs +++ b/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs @@ -4,111 +4,195 @@ namespace Rsk.AuthZen.Client { + /// + /// Provides a fluent builder interface for constructing AuthZen boxcar evaluation requests, + /// supporting the configuration of default subject, resource, action, context, and the addition of individual requests. + /// public interface IAuthZenBoxcarRequestBuilder { + /// + /// Sets the correlation identifier for the boxcar evaluation request. + /// + /// The correlation ID to associate with the request. + /// The current builder instance. IAuthZenBoxcarRequestBuilder SetCorrelationId(string correlationId); - + + /// + /// Sets the default subject for all evaluation requests in the boxcar. + /// + /// The subject identifier. + /// The subject type. + /// A property bag for adding subject properties. IAuthZenPropertyBag SetDefaultSubject(string id, string type); + + /// + /// Sets the default resource for all evaluation requests in the boxcar. + /// + /// The resource identifier. + /// The resource type. + /// A property bag for adding resource properties. IAuthZenPropertyBag SetDefaultResource(string id, string type); + + /// + /// Sets the default action for all evaluation requests in the boxcar. + /// + /// The action name. + /// A property bag for adding action properties. IAuthZenPropertyBag SetDefaultAction(string name); + + /// + /// Sets the default context for all evaluation requests in the boxcar. + /// + /// A property bag for adding context properties. IAuthZenPropertyBag SetDefaultContext(); - + + /// + /// Adds a new evaluation request to the boxcar. + /// + /// A builder for configuring the individual evaluation request. IAuthZenRequestBuilder AddRequest(); - + + /// + /// Builds the instance using the configured values and requests. + /// + /// The constructed . AuthZenBoxcarEvaluationRequest Build(); } + /// + /// Represents a boxcar evaluation request for AuthZen, including a correlation identifier and the request body. + /// public class AuthZenBoxcarEvaluationRequest { + /// + /// Gets the correlation identifier for tracking the boxcar evaluation request. + /// public string CorrelationId { get; internal set; } + + /// + /// Gets the body of the boxcar evaluation request, containing evaluation details. + /// public AuthZenBoxcarEvaluationBody Body { get; internal set; } } + /// + /// Provides a fluent builder for constructing AuthZen boxcar evaluation requests, + /// allowing configuration of default subject, resource, action, context, evaluation semantics, + /// and the addition of individual evaluation requests. + /// public class AuthZenBoxcarRequestBuilder : IAuthZenBoxcarRequestBuilder { private string correlationId; - private string defaultSubjectId; private string defaultSubjectType; private string defaultResourceId; private string defaultResourceType; private string defaultActionName; - + private AuthZenPropertyBag defaultSubjectProperties; private AuthZenPropertyBag defaultResourceProperties; private AuthZenPropertyBag defaultActionProperties; private AuthZenPropertyBag defaultContextProperties; - + private List evaluationBuilders = new List(); private AuthZenBoxcarOptions options; - + + /// + /// Sets the correlation identifier for the boxcar evaluation request. + /// + /// The correlation ID to associate with the request. + /// The current builder instance. public IAuthZenBoxcarRequestBuilder SetCorrelationId(string id) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Correlation ID must be provided", nameof(id)); - correlationId = id; - return this; } - + + /// + /// Sets the default subject for all evaluation requests in the boxcar. + /// + /// The subject identifier. + /// The subject type. + /// A property bag for adding subject properties. public IAuthZenPropertyBag SetDefaultSubject(string id, string type) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("ID must be provided", nameof(id)); if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); - defaultSubjectId = id; defaultSubjectType = type; - defaultSubjectProperties = new AuthZenPropertyBag(); return defaultSubjectProperties; } - + + /// + /// Sets the default resource for all evaluation requests in the boxcar. + /// + /// The resource identifier. + /// The resource type. + /// A property bag for adding resource properties. public IAuthZenPropertyBag SetDefaultResource(string id, string type) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("ID must be provided", nameof(id)); if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); - defaultResourceId = id; defaultResourceType = type; - defaultResourceProperties = new AuthZenPropertyBag(); return defaultResourceProperties; } - + + /// + /// Sets the default action for all evaluation requests in the boxcar. + /// + /// The action name. + /// A property bag for adding action properties. public IAuthZenPropertyBag SetDefaultAction(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name must be provided", nameof(name)); - defaultActionName = name; defaultActionProperties = new AuthZenPropertyBag(); return defaultActionProperties; } - + + /// + /// Sets the default context for all evaluation requests in the boxcar. + /// + /// A property bag for adding context properties. public IAuthZenPropertyBag SetDefaultContext() { defaultContextProperties = new AuthZenPropertyBag(); return defaultContextProperties; } - + + /// + /// Adds a new evaluation request to the boxcar. + /// + /// A builder for configuring the individual evaluation request. public IAuthZenRequestBuilder AddRequest() { var evaluationBuilder = new AuthZenBoxcarEvaluationRequestBuilder(); - evaluationBuilders.Add(evaluationBuilder); - return evaluationBuilder; } - + + /// + /// Sets the evaluation semantics for the boxcar request. + /// + /// The evaluation semantics to use. + /// The current builder instance. public IAuthZenBoxcarRequestBuilder SetEvaluationSemantics(BoxcarSemantics semantics) { options = new AuthZenBoxcarOptions() { Semantics = semantics }; - return this; } - + + /// + /// Builds the instance using the configured values and requests. + /// + /// The constructed . public AuthZenBoxcarEvaluationRequest Build() { var body = new AuthZenBoxcarEvaluationBody @@ -116,7 +200,7 @@ public AuthZenBoxcarEvaluationRequest Build() Evaluations = new List(), DefaultValues = new AuthZenEvaluationBody(), }; - + if (!string.IsNullOrWhiteSpace(defaultSubjectId) && !string.IsNullOrWhiteSpace(defaultSubjectType)) { body.DefaultValues.Subject = new AuthZenSubject @@ -124,13 +208,13 @@ public AuthZenBoxcarEvaluationRequest Build() Id = defaultSubjectId, Type = defaultSubjectType, }; - + if (defaultSubjectProperties is { IsEmpty: false }) { body.DefaultValues.Subject.Properties = defaultSubjectProperties.Build(); } } - + if (!string.IsNullOrWhiteSpace(defaultResourceId) && !string.IsNullOrWhiteSpace(defaultResourceType)) { body.DefaultValues.Resource = new AuthZenResource @@ -138,42 +222,42 @@ public AuthZenBoxcarEvaluationRequest Build() Id = defaultResourceId, Type = defaultResourceType, }; - + if (defaultResourceProperties is { IsEmpty: false }) { body.DefaultValues.Resource.Properties = defaultResourceProperties.Build(); } } - + if (!string.IsNullOrWhiteSpace(defaultActionName)) { body.DefaultValues.Action = new AuthZenAction { Name = defaultActionName, }; - + if (defaultActionProperties is { IsEmpty: false }) { body.DefaultValues.Action.Properties = defaultActionProperties.Build(); } } - + if (defaultContextProperties is { IsEmpty: false }) { body.DefaultValues.Context = defaultContextProperties.Build(); } - + foreach (var builder in evaluationBuilders.Where(eb => eb.HasValuesSet)) { var evaluationRequest = builder.Build(); body.Evaluations.Add(evaluationRequest); } - + if (options != null) { body.Options = options; } - + return new AuthZenBoxcarEvaluationRequest { CorrelationId = correlationId, @@ -182,6 +266,10 @@ public AuthZenBoxcarEvaluationRequest Build() } } + /// + /// Provides a builder for constructing an individual AuthZen evaluation request, + /// allowing the configuration of subject, resource, action, and context, each with their own properties. + /// internal class AuthZenBoxcarEvaluationRequestBuilder : IAuthZenRequestBuilder { private string subjectId; @@ -189,17 +277,27 @@ internal class AuthZenBoxcarEvaluationRequestBuilder : IAuthZenRequestBuilder private string resourceId; private string resourceType; private string actionName; - + private AuthZenPropertyBag subjectProperties; private AuthZenPropertyBag resourceProperties; private AuthZenPropertyBag actionProperties; private AuthZenPropertyBag contextProperties; + + /// + /// Gets a value indicating whether any values have been set for this evaluation request. + /// public bool HasValuesSet => !string.IsNullOrWhiteSpace(subjectId) || !string.IsNullOrWhiteSpace(resourceId) || !string.IsNullOrWhiteSpace(actionName) || (contextProperties?.IsEmpty == false); - + + /// + /// Sets the subject for the evaluation request. + /// + /// The subject identifier. + /// The subject type. + /// A property bag for adding subject properties. public IAuthZenPropertyBag SetSubject(string id, string type) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id must be provided", nameof(id)); @@ -210,37 +308,56 @@ public IAuthZenPropertyBag SetSubject(string id, string type) subjectProperties = new AuthZenPropertyBag(); return subjectProperties; } - + + /// + /// Sets the resource for the evaluation request. + /// + /// The resource identifier. + /// The resource type. + /// A property bag for adding resource properties. public IAuthZenPropertyBag SetResource(string id, string type) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id must be provided", nameof(id)); if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type must be provided", nameof(type)); - + resourceId = id; resourceType = type; resourceProperties = new AuthZenPropertyBag(); return resourceProperties; } - + + /// + /// Sets the action for the evaluation request. + /// + /// The action name. + /// A property bag for adding action properties. public IAuthZenPropertyBag SetAction(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name must be provided", nameof(name)); - + actionName = name; actionProperties = new AuthZenPropertyBag(); return actionProperties; } - + + /// + /// Sets the context for the evaluation request. + /// + /// A property bag for adding context properties. public IAuthZenPropertyBag SetContext() { contextProperties = new AuthZenPropertyBag(); return contextProperties; } - + + /// + /// Builds the instance using the configured values. + /// + /// The constructed . public AuthZenEvaluationBody Build() { var request = new AuthZenEvaluationBody(); - + if (subjectId != null) { request.Subject = new AuthZenSubject @@ -254,7 +371,7 @@ public AuthZenEvaluationBody Build() request.Subject.Properties = subjectProperties.Build(); } } - + if (resourceId != null) { request.Resource = new AuthZenResource @@ -262,13 +379,13 @@ public AuthZenEvaluationBody Build() Id = resourceId, Type = resourceType, }; - + if (!resourceProperties.IsEmpty) { request.Resource.Properties = resourceProperties.Build(); } } - + if (actionName != null) { request.Action = new AuthZenAction @@ -281,12 +398,12 @@ public AuthZenEvaluationBody Build() request.Action.Properties = actionProperties.Build(); } } - + if (contextProperties != null && !contextProperties.IsEmpty) { request.Context = contextProperties.Build(); } - + return request; } } diff --git a/Rsk.AuthZen.Client/IAuthZenClient.cs b/Rsk.AuthZen.Client/IAuthZenClient.cs index 629137b..e5aff4a 100644 --- a/Rsk.AuthZen.Client/IAuthZenClient.cs +++ b/Rsk.AuthZen.Client/IAuthZenClient.cs @@ -3,10 +3,27 @@ namespace Rsk.AuthZen.Client { + /// + /// Defines methods for evaluating authorization requests using the AuthZen service. + /// public interface IAuthZenClient { + /// + /// Evaluates a single authorization request. + /// + /// The authorization evaluation request. + /// + /// A task that represents the asynchronous operation. The task result contains the evaluation response. + /// Task Evaluate(AuthZenEvaluationRequest request); - + + /// + /// Evaluates a boxcar (batch) authorization request. + /// + /// The boxcar evaluation request containing multiple evaluations. + /// + /// A task that represents the asynchronous operation. The task result contains the boxcar evaluation response. + /// Task Evaluate(AuthZenBoxcarEvaluationRequest request); } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs b/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs index 616b393..6b41400 100644 --- a/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs +++ b/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs @@ -2,51 +2,141 @@ namespace Rsk.AuthZen.Client { + /// + /// Provides a builder interface for constructing an AuthZen evaluation request, + /// allowing the configuration of subject, resource, action, and context, each with their own properties. + /// public interface IAuthZenRequestBuilder { + /// + /// Sets the subject for the evaluation request. + /// + /// The subject identifier. + /// The subject type. + /// A property bag for adding subject properties. IAuthZenPropertyBag SetSubject(string id, string type); + + /// + /// Sets the resource for the evaluation request. + /// + /// The resource identifier. + /// The resource type. + /// A property bag for adding resource properties. IAuthZenPropertyBag SetResource(string id, string type); + + /// + /// Sets the action for the evaluation request. + /// + /// The action name. + /// A property bag for adding action properties. IAuthZenPropertyBag SetAction(string name); + + /// + /// Sets the context for the evaluation request. + /// + /// A property bag for adding context properties. IAuthZenPropertyBag SetContext(); } + /// + /// Provides a fluent builder interface for constructing a single AuthZen evaluation request, + /// supporting the configuration of correlation ID, subject, resource, action, and context. + /// public interface IAuthZenSingleRequestBuilder : IAuthZenRequestBuilder { + /// + /// Sets the correlation identifier for the evaluation request. + /// + /// The correlation ID to associate with the request. + /// The current builder instance. IAuthZenSingleRequestBuilder SetCorrelationId(string correlationId); - + + /// + /// Builds the instance using the configured values. + /// + /// The constructed . AuthZenEvaluationRequest Build(); } + /// + /// Represents a single AuthZen evaluation request, including a correlation identifier and the request body. + /// public class AuthZenEvaluationRequest { + /// + /// Gets the correlation identifier for tracking the evaluation request. + /// public string CorrelationId { get; internal set; } + + /// + /// Gets the body of the evaluation request, containing evaluation details. + /// public AuthZenEvaluationBody Body { get; internal set; } } + /// + /// Represents a property bag for storing key-value pairs used in AuthZen evaluation requests. + /// Provides methods to add properties and check if the bag is empty. + /// public interface IAuthZenPropertyBag { + /// + /// Adds a property with the specified name and value to the property bag. + /// + /// The name of the property to add. + /// The value of the property. + /// The current property bag instance for fluent chaining. IAuthZenPropertyBag Add(string name, object value); + + /// + /// Gets a value indicating whether the property bag is empty. + /// bool IsEmpty { get; } } + /// + /// Extends to provide a method for building + /// the property bag into a dictionary of key-value pairs for use in AuthZen evaluation requests. + /// internal interface IAuthZenPropertyBuilder : IAuthZenPropertyBag { + /// + /// Builds and returns the property bag as a dictionary of key-value pairs. + /// + /// A dictionary containing all properties added to the bag. Dictionary Build(); } + /// + /// Implements to provide a property bag for storing + /// key-value pairs used in AuthZen evaluation requests. Supports adding properties, + /// checking if the bag is empty, and building the bag into a dictionary. + /// public class AuthZenPropertyBag : IAuthZenPropertyBuilder { private readonly Dictionary properties = new Dictionary(); - + + /// + /// Gets a value indicating whether the property bag is empty. + /// public bool IsEmpty => properties.Count == 0; - + + /// + /// Adds a property with the specified name and value to the property bag. + /// + /// The name of the property to add. + /// The value of the property. + /// The current property bag instance for fluent chaining. public IAuthZenPropertyBag Add(string name, object value) { properties[name] = value; - return this; } - + + /// + /// Builds and returns the property bag as a dictionary of key-value pairs. + /// + /// A dictionary containing all properties added to the bag. public Dictionary Build() { return properties; diff --git a/Rsk.AuthZen.sln.DotSettings.user b/Rsk.AuthZen.sln.DotSettings.user index 3b80a40..9f144f1 100644 --- a/Rsk.AuthZen.sln.DotSettings.user +++ b/Rsk.AuthZen.sln.DotSettings.user @@ -1,4 +1,5 @@  + ForceIncluded ForceIncluded ForceIncluded <SessionState ContinuousTestingMode="0" IsActive="True" Name="AuthZenClientTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> From 0b3799eccd4fff3f9153976c4cc4ac7f0330c591 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 18 Aug 2025 15:47:08 +0100 Subject: [PATCH 06/33] Fix typo --- Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs | 2 +- Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs | 2 +- Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs b/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs index 98f145c..122b9f4 100644 --- a/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs +++ b/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs @@ -22,6 +22,6 @@ public void ToDto_WhenCalled_ShouldTranslateSemanticsCorrectly(BoxcarSemantics s AuthZenBoxcarOptionsDto dto = sut.ToDto(); - dto.Evaluation_semantics.Should().Be(expectedDtoValue); + dto.Evaluations_semantic.Should().Be(expectedDtoValue); } } \ No newline at end of file diff --git a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs index 07c23bf..188aaaf 100644 --- a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs +++ b/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs @@ -138,7 +138,7 @@ internal AuthZenBoxcarOptionsDto ToDto() { return new AuthZenBoxcarOptionsDto { - Evaluation_semantics = ConvertSemantics(Semantics) + Evaluations_semantic = ConvertSemantics(Semantics) }; } diff --git a/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs b/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs index 0de7591..c223d24 100644 --- a/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs +++ b/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs @@ -8,6 +8,6 @@ internal class AuthZenBoxcarRequestMessageDto : AuthZenRequestMessageDto internal class AuthZenBoxcarOptionsDto { - public string Evaluation_semantics { get; set; } + public string Evaluations_semantic { get; set; } } } \ No newline at end of file From f91c004ea667b282e1793646ad6033337e7b50b2 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Fri, 29 Aug 2025 14:21:20 +0100 Subject: [PATCH 07/33] Relocate cs code --- src/.DS_Store | Bin 0 -> 6148 bytes README.md => src/README.md | 0 .../AuthZenBoxCarOptionsTests.cs | 0 .../AuthZenBoxcarEvaluationBodyTests.cs | 0 .../AuthZenBoxcarRequestBuilderTests.cs | 0 .../AuthZenClientTests.cs | 0 .../AuthZenPropertyBagTests.cs | 0 .../AuthZenSingleRequestBuilderTests.cs | 0 .../Rsk.AuthZen.Client.Test.csproj | 0 .../Rsk.AuthZen.Client}/AssemblyInfo.cs | 0 .../Rsk.AuthZen.Client}/AuthZenAction.cs | 0 .../AuthZenBoxcarEvaluationBody.cs | 0 .../Rsk.AuthZen.Client}/AuthZenClient.cs | 0 .../Rsk.AuthZen.Client}/AuthZenEvaluationBody.cs | 0 .../AuthZenRequestFailureException.cs | 0 .../Rsk.AuthZen.Client}/AuthZenResource.cs | 0 .../Rsk.AuthZen.Client}/AuthZenResponse.cs | 0 .../AuthZenSingleRequestBuilder.cs | 0 .../Rsk.AuthZen.Client}/AuthZenSubject.cs | 0 .../DTOs/AuthZenBoxcarRequestMessageDto.cs | 0 .../DTOs/AuthZenRequestMessageDto.cs | 0 .../DTOs/AuthZenResponseDto.cs | 0 .../Rsk.AuthZen.Client}/Decision.cs | 0 .../IAuthZenBoxcarRequestBuilder.cs | 0 .../Rsk.AuthZen.Client}/IAuthZenClient.cs | 0 .../IAuthZenSingleRequestBuilder.cs | 0 .../Rsk.AuthZen.Client.csproj | 0 Rsk.AuthZen.sln => src/Rsk.AuthZen.sln | 0 .../Rsk.AuthZen.sln.DotSettings.user | 0 29 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/.DS_Store rename README.md => src/README.md (100%) rename {Rsk.AuthZen.Client.Test => src/Rsk.AuthZen.Client.Test}/AuthZenBoxCarOptionsTests.cs (100%) rename {Rsk.AuthZen.Client.Test => src/Rsk.AuthZen.Client.Test}/AuthZenBoxcarEvaluationBodyTests.cs (100%) rename {Rsk.AuthZen.Client.Test => src/Rsk.AuthZen.Client.Test}/AuthZenBoxcarRequestBuilderTests.cs (100%) rename {Rsk.AuthZen.Client.Test => src/Rsk.AuthZen.Client.Test}/AuthZenClientTests.cs (100%) rename {Rsk.AuthZen.Client.Test => src/Rsk.AuthZen.Client.Test}/AuthZenPropertyBagTests.cs (100%) rename {Rsk.AuthZen.Client.Test => src/Rsk.AuthZen.Client.Test}/AuthZenSingleRequestBuilderTests.cs (100%) rename {Rsk.AuthZen.Client.Test => src/Rsk.AuthZen.Client.Test}/Rsk.AuthZen.Client.Test.csproj (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AssemblyInfo.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenAction.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenBoxcarEvaluationBody.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenClient.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenEvaluationBody.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenRequestFailureException.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenResource.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenResponse.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenSingleRequestBuilder.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/AuthZenSubject.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/DTOs/AuthZenBoxcarRequestMessageDto.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/DTOs/AuthZenRequestMessageDto.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/DTOs/AuthZenResponseDto.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/Decision.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/IAuthZenBoxcarRequestBuilder.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/IAuthZenClient.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/IAuthZenSingleRequestBuilder.cs (100%) rename {Rsk.AuthZen.Client => src/Rsk.AuthZen.Client}/Rsk.AuthZen.Client.csproj (100%) rename Rsk.AuthZen.sln => src/Rsk.AuthZen.sln (100%) rename Rsk.AuthZen.sln.DotSettings.user => src/Rsk.AuthZen.sln.DotSettings.user (100%) diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..49b0edfc49080984fd922a0c8356c1fcabe412fb GIT binary patch literal 6148 zcmeHK%}N6?5T4N@3toEkm`AX05bJsqJS^U|7VM$RE`{FncEz{x5&Y&yVKpFlkSa5f ze3Q)2X1`^V3=#2izg`f{iKsynWKk+2(^b=nJI{cuId(K|pBLTJ(2tcw$2cWxKcj2v zXhV-wU;o734{f{J4lBepFSqx1ua}3b&pG;G{jJZq`BOX{gqP`3_rS^@y&8m$6dY6*!+j@hvo!UACn1zIS3iNO|* z`Q(1tu^3u7u@@igPkt|6Sf7skN!^LFVf4X3Ffe7{)P{38|8MZiREzv+NQ{DkVBnuI zz_Ye%mv|{ZTfe-XoV5w<0Zl~wiYO51lS=>wvX5M-(d$ I27ZBoH{tUuWdHyG literal 0 HcmV?d00001 diff --git a/README.md b/src/README.md similarity index 100% rename from README.md rename to src/README.md diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs b/src/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs similarity index 100% rename from Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs rename to src/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs b/src/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs similarity index 100% rename from Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs rename to src/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs diff --git a/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs b/src/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs similarity index 100% rename from Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs rename to src/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs diff --git a/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/src/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs similarity index 100% rename from Rsk.AuthZen.Client.Test/AuthZenClientTests.cs rename to src/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs diff --git a/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs b/src/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs similarity index 100% rename from Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs rename to src/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs diff --git a/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs b/src/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs similarity index 100% rename from Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs rename to src/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs diff --git a/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj b/src/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj similarity index 100% rename from Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj rename to src/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj diff --git a/Rsk.AuthZen.Client/AssemblyInfo.cs b/src/Rsk.AuthZen.Client/AssemblyInfo.cs similarity index 100% rename from Rsk.AuthZen.Client/AssemblyInfo.cs rename to src/Rsk.AuthZen.Client/AssemblyInfo.cs diff --git a/Rsk.AuthZen.Client/AuthZenAction.cs b/src/Rsk.AuthZen.Client/AuthZenAction.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenAction.cs rename to src/Rsk.AuthZen.Client/AuthZenAction.cs diff --git a/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs b/src/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs rename to src/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs diff --git a/Rsk.AuthZen.Client/AuthZenClient.cs b/src/Rsk.AuthZen.Client/AuthZenClient.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenClient.cs rename to src/Rsk.AuthZen.Client/AuthZenClient.cs diff --git a/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs b/src/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenEvaluationBody.cs rename to src/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs diff --git a/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs b/src/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenRequestFailureException.cs rename to src/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs diff --git a/Rsk.AuthZen.Client/AuthZenResource.cs b/src/Rsk.AuthZen.Client/AuthZenResource.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenResource.cs rename to src/Rsk.AuthZen.Client/AuthZenResource.cs diff --git a/Rsk.AuthZen.Client/AuthZenResponse.cs b/src/Rsk.AuthZen.Client/AuthZenResponse.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenResponse.cs rename to src/Rsk.AuthZen.Client/AuthZenResponse.cs diff --git a/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs b/src/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs rename to src/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs diff --git a/Rsk.AuthZen.Client/AuthZenSubject.cs b/src/Rsk.AuthZen.Client/AuthZenSubject.cs similarity index 100% rename from Rsk.AuthZen.Client/AuthZenSubject.cs rename to src/Rsk.AuthZen.Client/AuthZenSubject.cs diff --git a/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs b/src/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs similarity index 100% rename from Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs rename to src/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs diff --git a/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs b/src/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs similarity index 100% rename from Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs rename to src/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs diff --git a/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs b/src/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs similarity index 100% rename from Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs rename to src/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs diff --git a/Rsk.AuthZen.Client/Decision.cs b/src/Rsk.AuthZen.Client/Decision.cs similarity index 100% rename from Rsk.AuthZen.Client/Decision.cs rename to src/Rsk.AuthZen.Client/Decision.cs diff --git a/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs b/src/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs similarity index 100% rename from Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs rename to src/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs diff --git a/Rsk.AuthZen.Client/IAuthZenClient.cs b/src/Rsk.AuthZen.Client/IAuthZenClient.cs similarity index 100% rename from Rsk.AuthZen.Client/IAuthZenClient.cs rename to src/Rsk.AuthZen.Client/IAuthZenClient.cs diff --git a/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs b/src/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs similarity index 100% rename from Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs rename to src/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs diff --git a/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj b/src/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj similarity index 100% rename from Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj rename to src/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj diff --git a/Rsk.AuthZen.sln b/src/Rsk.AuthZen.sln similarity index 100% rename from Rsk.AuthZen.sln rename to src/Rsk.AuthZen.sln diff --git a/Rsk.AuthZen.sln.DotSettings.user b/src/Rsk.AuthZen.sln.DotSettings.user similarity index 100% rename from Rsk.AuthZen.sln.DotSettings.user rename to src/Rsk.AuthZen.sln.DotSettings.user From bc7e5bd255572bd592441549bdcf6b794aeec0db Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Fri, 29 Aug 2025 14:25:08 +0100 Subject: [PATCH 08/33] Move to C# lib to folder --- .gitignore | 3 ++- src/.DS_Store | Bin 6148 -> 0 bytes src/{ => CSharp}/README.md | 0 .../AuthZenBoxCarOptionsTests.cs | 0 .../AuthZenBoxcarEvaluationBodyTests.cs | 0 .../AuthZenBoxcarRequestBuilderTests.cs | 0 .../AuthZenClientTests.cs | 0 .../AuthZenPropertyBagTests.cs | 0 .../AuthZenSingleRequestBuilderTests.cs | 0 .../Rsk.AuthZen.Client.Test.csproj | 0 .../Rsk.AuthZen.Client/AssemblyInfo.cs | 0 .../Rsk.AuthZen.Client/AuthZenAction.cs | 0 .../AuthZenBoxcarEvaluationBody.cs | 0 .../Rsk.AuthZen.Client/AuthZenClient.cs | 0 .../Rsk.AuthZen.Client/AuthZenEvaluationBody.cs | 0 .../AuthZenRequestFailureException.cs | 0 .../Rsk.AuthZen.Client/AuthZenResource.cs | 0 .../Rsk.AuthZen.Client/AuthZenResponse.cs | 0 .../AuthZenSingleRequestBuilder.cs | 0 .../Rsk.AuthZen.Client/AuthZenSubject.cs | 0 .../DTOs/AuthZenBoxcarRequestMessageDto.cs | 0 .../DTOs/AuthZenRequestMessageDto.cs | 0 .../DTOs/AuthZenResponseDto.cs | 0 src/{ => CSharp}/Rsk.AuthZen.Client/Decision.cs | 0 .../IAuthZenBoxcarRequestBuilder.cs | 0 .../Rsk.AuthZen.Client/IAuthZenClient.cs | 0 .../IAuthZenSingleRequestBuilder.cs | 0 .../Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj | 0 src/{ => CSharp}/Rsk.AuthZen.sln | 0 .../Rsk.AuthZen.sln.DotSettings.user | 0 30 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 src/.DS_Store rename src/{ => CSharp}/README.md (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AssemblyInfo.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenAction.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenClient.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenResource.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenResponse.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/AuthZenSubject.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/Decision.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/IAuthZenClient.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs (100%) rename src/{ => CSharp}/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj (100%) rename src/{ => CSharp}/Rsk.AuthZen.sln (100%) rename src/{ => CSharp}/Rsk.AuthZen.sln.DotSettings.user (100%) diff --git a/.gitignore b/.gitignore index add57be..8c7127e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +**/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 49b0edfc49080984fd922a0c8356c1fcabe412fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}N6?5T4N@3toEkm`AX05bJsqJS^U|7VM$RE`{FncEz{x5&Y&yVKpFlkSa5f ze3Q)2X1`^V3=#2izg`f{iKsynWKk+2(^b=nJI{cuId(K|pBLTJ(2tcw$2cWxKcj2v zXhV-wU;o734{f{J4lBepFSqx1ua}3b&pG;G{jJZq`BOX{gqP`3_rS^@y&8m$6dY6*!+j@hvo!UACn1zIS3iNO|* z`Q(1tu^3u7u@@igPkt|6Sf7skN!^LFVf4X3Ffe7{)P{38|8MZiREzv+NQ{DkVBnuI zz_Ye%mv|{ZTfe-XoV5w<0Zl~wiYO51lS=>wvX5M-(d$ I27ZBoH{tUuWdHyG diff --git a/src/README.md b/src/CSharp/README.md similarity index 100% rename from src/README.md rename to src/CSharp/README.md diff --git a/src/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs similarity index 100% rename from src/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs rename to src/CSharp/Rsk.AuthZen.Client.Test/AuthZenBoxCarOptionsTests.cs diff --git a/src/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs similarity index 100% rename from src/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs rename to src/CSharp/Rsk.AuthZen.Client.Test/AuthZenBoxcarEvaluationBodyTests.cs diff --git a/src/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs similarity index 100% rename from src/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs rename to src/CSharp/Rsk.AuthZen.Client.Test/AuthZenBoxcarRequestBuilderTests.cs diff --git a/src/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs similarity index 100% rename from src/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs rename to src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs diff --git a/src/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs similarity index 100% rename from src/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs rename to src/CSharp/Rsk.AuthZen.Client.Test/AuthZenPropertyBagTests.cs diff --git a/src/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs similarity index 100% rename from src/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs rename to src/CSharp/Rsk.AuthZen.Client.Test/AuthZenSingleRequestBuilderTests.cs diff --git a/src/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj b/src/CSharp/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj similarity index 100% rename from src/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj rename to src/CSharp/Rsk.AuthZen.Client.Test/Rsk.AuthZen.Client.Test.csproj diff --git a/src/Rsk.AuthZen.Client/AssemblyInfo.cs b/src/CSharp/Rsk.AuthZen.Client/AssemblyInfo.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AssemblyInfo.cs rename to src/CSharp/Rsk.AuthZen.Client/AssemblyInfo.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenAction.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenAction.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenAction.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenAction.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenBoxcarEvaluationBody.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenClient.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenClient.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenClient.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenClient.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenEvaluationBody.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenRequestFailureException.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenResource.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenResource.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenResource.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenResource.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenResponse.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenResponse.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenResponse.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenResponse.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenSingleRequestBuilder.cs diff --git a/src/Rsk.AuthZen.Client/AuthZenSubject.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenSubject.cs similarity index 100% rename from src/Rsk.AuthZen.Client/AuthZenSubject.cs rename to src/CSharp/Rsk.AuthZen.Client/AuthZenSubject.cs diff --git a/src/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs b/src/CSharp/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs similarity index 100% rename from src/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs rename to src/CSharp/Rsk.AuthZen.Client/DTOs/AuthZenBoxcarRequestMessageDto.cs diff --git a/src/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs b/src/CSharp/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs similarity index 100% rename from src/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs rename to src/CSharp/Rsk.AuthZen.Client/DTOs/AuthZenRequestMessageDto.cs diff --git a/src/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs b/src/CSharp/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs similarity index 100% rename from src/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs rename to src/CSharp/Rsk.AuthZen.Client/DTOs/AuthZenResponseDto.cs diff --git a/src/Rsk.AuthZen.Client/Decision.cs b/src/CSharp/Rsk.AuthZen.Client/Decision.cs similarity index 100% rename from src/Rsk.AuthZen.Client/Decision.cs rename to src/CSharp/Rsk.AuthZen.Client/Decision.cs diff --git a/src/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs b/src/CSharp/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs similarity index 100% rename from src/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs rename to src/CSharp/Rsk.AuthZen.Client/IAuthZenBoxcarRequestBuilder.cs diff --git a/src/Rsk.AuthZen.Client/IAuthZenClient.cs b/src/CSharp/Rsk.AuthZen.Client/IAuthZenClient.cs similarity index 100% rename from src/Rsk.AuthZen.Client/IAuthZenClient.cs rename to src/CSharp/Rsk.AuthZen.Client/IAuthZenClient.cs diff --git a/src/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs b/src/CSharp/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs similarity index 100% rename from src/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs rename to src/CSharp/Rsk.AuthZen.Client/IAuthZenSingleRequestBuilder.cs diff --git a/src/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj b/src/CSharp/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj similarity index 100% rename from src/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj rename to src/CSharp/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj diff --git a/src/Rsk.AuthZen.sln b/src/CSharp/Rsk.AuthZen.sln similarity index 100% rename from src/Rsk.AuthZen.sln rename to src/CSharp/Rsk.AuthZen.sln diff --git a/src/Rsk.AuthZen.sln.DotSettings.user b/src/CSharp/Rsk.AuthZen.sln.DotSettings.user similarity index 100% rename from src/Rsk.AuthZen.sln.DotSettings.user rename to src/CSharp/Rsk.AuthZen.sln.DotSettings.user From e6077af41d1ddc74a2849cf36db14d3d480cea1f Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 1 Sep 2025 15:07:22 +0100 Subject: [PATCH 09/33] Implement metadata endpoint Use response from metadata endpoint for evaluation(s) request URIs --- .gitignore | 2 + .../AuthZenClientTests.cs | 1663 +++++++++++++++-- .../Rsk.AuthZen.Client/AuthZenClient.cs | 64 +- .../AuthZenMetadataResponse.cs | 17 + .../Rsk.AuthZen.Client/IAuthZenClient.cs | 8 + src/CSharp/Rsk.AuthZen.sln.DotSettings.user | 6 + 6 files changed, 1589 insertions(+), 171 deletions(-) create mode 100644 src/CSharp/Rsk.AuthZen.Client/AuthZenMetadataResponse.cs diff --git a/.gitignore b/.gitignore index 8c7127e..cd86239 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ obj/ riderModule.iml /_ReSharper.Caches/ **/.DS_Store + +src/CSharp/.idea/.idea.Rsk.AuthZen/.idea/ diff --git a/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs index 9eb9af8..ee69fa9 100644 --- a/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs +++ b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs @@ -23,7 +23,33 @@ public class AuthZenClientTests HttpClient httpClient; AuthZenClientOptions optionsValue; Mock> options; + + private const string metadataPdpUri = "https://examplepdp.com"; + private const string metadataEvaluationEndpoint = "https://examplepdp.com/evaluation"; + private const string metadataEvaluationsEndpoint = "https://examplepdp.com/evaluations"; + private const string metadataSubjectSearchEndpoint = "https://examplepdp.com/subject-search"; + private const string metadataActionSearchEndpoint = "https://examplepdp.com/action-search"; + private const string metadataResourceSearchEndpoint = "https://examplepdp.com/resource-search"; + private const string simpleMetadataResponse = + $$""" + { + "policy_decision_point": "{{metadataPdpUri}}", + "access_evaluation_endpoint": "{{metadataEvaluationEndpoint}}", + "access_evaluations_endpoint": "{{metadataEvaluationsEndpoint}}", + "search_subject_endpoint": "{{metadataSubjectSearchEndpoint}}", + "search_action_endpoint": "{{metadataActionSearchEndpoint}}", + "search_resource_endpoint": "{{metadataResourceSearchEndpoint}}" + } + """; + + private const string evaluationOnlyMetadataResponse = + $$""" + { + "policy_decision_point": "{{metadataPdpUri}}" + } + """; + private const string simpleEvaluationResponse = """ { @@ -62,20 +88,38 @@ private AuthZenClient CreateSut() private async Task VerifyMissingRequestPartOmitsElement(AuthZenEvaluationRequest singleEvaluationRequest, string expectedMissingElement) { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleEvaluationResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); }); var sut = CreateSut(); await sut.Evaluate(singleEvaluationRequest); - string sentContent = await requestSent.Content.ReadAsStringAsync(); + string sentContent = await sentRequests[1].Content.ReadAsStringAsync(); var json = JsonDocument.Parse(sentContent); @@ -167,17 +211,143 @@ public void ctor_WhenCalled_ShouldCreateHttpClientWithBaseUrlSetToThatFromOption client.BaseAddress.Should().Be(uri); } + + [Fact] + public async Task Evaluate_WhenInitiallyCalled_ShouldGetMetadataEndpoint() + { + List sentRequests = new List(); + + int calls = 0; + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenEvaluationRequest + { + Body = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }; + + await sut.Evaluate(evaluationRequest); + + sentRequests.Should().HaveCount(2); + + sentRequests[0].Method.Should().Be(HttpMethod.Get); + sentRequests[0].RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.MetadataEndpointUri}"); + } + + [Fact] + public async Task Evaluate_WhenCalledMultipleTimes_ShouldReuseMetadataFromFirstCall() + { + List sentRequests = new List(); + + int calls = 0; + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenEvaluationRequest + { + Body = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }; + + await sut.Evaluate(evaluationRequest); + await sut.Evaluate(evaluationRequest); + await sut.Evaluate(evaluationRequest); + + sentRequests.Should().HaveCount(4); + + sentRequests + .Count(r => r.RequestUri.ToString() == $"{optionsValue.AuthorizationUrl}/{AuthZenClient.MetadataEndpointUri}") + .Should() + .Be(1); + } [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostToCorrectEndpoint() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleEvaluationResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); }); var sut = CreateSut(); @@ -196,22 +366,40 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostToCorrectEnd await sut.Evaluate(evaluationRequest); - requestSent.Should().NotBeNull(); - requestSent.Method.Should().Be(HttpMethod.Post); - requestSent.RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.UriBase}/{AuthZenClient.EvaluationUri}"); - requestSent.Content.Headers.ContentType.MediaType.Should().Be("application/json"); + sentRequests.Should().HaveCount(2); + sentRequests[1].Method.Should().Be(HttpMethod.Post); + sentRequests[1].RequestUri.Should().Be(metadataEvaluationEndpoint); + sentRequests[1].Content.Headers.ContentType.MediaType.Should().Be("application/json"); } [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationWithCorrelationId_ShouldAddRequestIdHeaderToRequest() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleEvaluationResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); }); var sut = CreateSut(); @@ -231,20 +419,38 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationWithCorrelationId_Shoul await sut.Evaluate(evaluationRequest); - requestSent.Headers.Should().ContainSingle(h => h.Key == "X-Request-ID" + sentRequests[1].Headers.Should().ContainSingle(h => h.Key == "X-Request-ID" && h.Value.Contains(evaluationRequest.CorrelationId)); } [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRequestCorrectly() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleEvaluationResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); }); var sut = CreateSut(); @@ -292,7 +498,7 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldPostSerializedRe await sut.Evaluate(evaluationRequest); - string sentContent = await requestSent.Content.ReadAsStringAsync(); + string sentContent = await sentRequests[1].Content.ReadAsStringAsync(); AuthZenRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); @@ -464,11 +670,32 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldParseDecisionCor "decision": {{jsonDecision}} } """; + + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(response) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); }); var sut = CreateSut(); @@ -511,11 +738,31 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldExtractContextCo } """; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(response) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); }); var sut = CreateSut(); @@ -540,10 +787,29 @@ public async Task Evaluate_WhenCalledWithSingleEvaluation_ShouldExtractContextCo [Fact] public async Task Evaluate_WhenCalledWithSingleEvaluationAndRequestFails_ShouldThrowAuthZenRequestFailureException() { + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + }); var sut = CreateSut(); @@ -574,12 +840,32 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndResponseContainsRequ } """; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(response), - Headers = { { "X-Request-ID", expectedRequestId } } + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response), + Headers = { { "X-Request-ID", expectedRequestId } } + }); }); var sut = CreateSut(); @@ -600,17 +886,35 @@ public async Task Evaluate_WhenCalledWithSingleEvaluationAndResponseContainsRequ authZenResponse.CorrelationId.Should().Be(expectedRequestId); } - + [Fact] - public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPostToCorrectEndpoint() + public async Task Evaluate_WhenCalledWithBoxcarRequest_ShouldGetMetadataToDetermineEndpoint() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleBoxcarResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); @@ -634,32 +938,48 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos } }; - await sut.Evaluate(evaluationRequest); - requestSent.Should().NotBeNull(); - requestSent.Method.Should().Be(HttpMethod.Post); - requestSent.RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.UriBase}/{AuthZenClient.BoxcarUri}"); - requestSent.Content.Headers.ContentType.MediaType.Should().Be("application/json"); + sentRequests.Should().HaveCount(2); + + sentRequests[0].Method.Should().Be(HttpMethod.Get); + sentRequests[0].RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.MetadataEndpointUri}"); } [Fact] - public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrelationId_ShouldAddRequestIdHeaderToRequest() + public async Task Evaluate_WhenCalledForBoxcarMultipleTimes_ShouldReuseMetadataFromFirstCall() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleBoxcarResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); var evaluationRequest = new AuthZenBoxcarEvaluationRequest() { - CorrelationId = Guid.NewGuid().ToString(), Body = new AuthZenBoxcarEvaluationBody() { Evaluations = new List @@ -674,27 +994,49 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrel } }, DefaultValues = new AuthZenEvaluationBody() - }, + } }; + await sut.Evaluate(evaluationRequest); + await sut.Evaluate(evaluationRequest); await sut.Evaluate(evaluationRequest); - requestSent.Headers + sentRequests.Should().HaveCount(4); + + sentRequests + .Count(r => r.RequestUri.ToString() == $"{optionsValue.AuthorizationUrl}/{AuthZenClient.MetadataEndpointUri}") .Should() - .ContainSingle(h => h.Key == "X-Request-ID" - && h.Value.Contains(evaluationRequest.CorrelationId)); + .Be(1); } [Fact] - public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPostSerializedRequestCorrectly() + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPostToCorrectEndpoint() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleBoxcarResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); @@ -703,7 +1045,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos { Body = new AuthZenBoxcarEvaluationBody() { - Evaluations = new List() + Evaluations = new List { new() { @@ -715,56 +1057,56 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPos } }, DefaultValues = new AuthZenEvaluationBody() - }, - CorrelationId = Guid.NewGuid().ToString(), + } }; - + await sut.Evaluate(evaluationRequest); - string sentContent = await requestSent.Content.ReadAsStringAsync(); - - AuthZenBoxcarRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, - new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); - - AdjustBoxcarRequestSerialization(deserializedRequest); - - deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Body.ToDto()); + sentRequests[1].Should().NotBeNull(); + sentRequests[1].Method.Should().Be(HttpMethod.Post); + sentRequests[1].RequestUri.Should().Be(metadataEvaluationsEndpoint); + sentRequests[1].Content.Headers.ContentType.MediaType.Should().Be("application/json"); } - - [Theory] - [InlineData("true", Decision.Permit)] - [InlineData("false", Decision.Deny)] - public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldParseDecisionCorrectly(string jsonDecision, Decision expectedDecision) + + [Fact] + public async Task Evaluate_WhenBoxcarRequestIsNotSupportedByServer_ShouldThrowNotSupportedException() { - string response = - $$""" - { - "evaluations": [ - { - "decision": {{jsonDecision}} - }, - { - "decision": {{jsonDecision}} - } - ] - } - """; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => { - Content = new StringContent(response) + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(evaluationOnlyMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest { - Body = new AuthZenBoxcarEvaluationBody() + Body = new AuthZenBoxcarEvaluationBody { - Evaluations = new List() + Evaluations = new List { - new AuthZenEvaluationBody() + new() { Subject = new AuthZenSubject { @@ -772,14 +1114,210 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPar Type = "aerfbqret" } } - } + }, + DefaultValues = new AuthZenEvaluationBody() } }; - AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest); - - authZenResponse.Evaluations.Should().BeEquivalentTo(new List() - { + var act = async () => await sut.Evaluate(evaluationRequest); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsWithCorrelationId_ShouldAddRequestIdHeaderToRequest() + { + List sentRequests = new List(); + + int calls = 0; + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + { + CorrelationId = Guid.NewGuid().ToString(), + Body = new AuthZenBoxcarEvaluationBody() + { + Evaluations = new List + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenEvaluationBody() + }, + }; + + await sut.Evaluate(evaluationRequest); + + sentRequests[1].Headers + .Should() + .ContainSingle(h => h.Key == "X-Request-ID" + && h.Value.Contains(evaluationRequest.CorrelationId)); + } + + [Fact] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldPostSerializedRequestCorrectly() + { + List sentRequests = new List(); + + int calls = 0; + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + { + Body = new AuthZenBoxcarEvaluationBody() + { + Evaluations = new List() + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenEvaluationBody() + }, + CorrelationId = Guid.NewGuid().ToString(), + }; + + await sut.Evaluate(evaluationRequest); + + string sentContent = await sentRequests[1].Content.ReadAsStringAsync(); + + AuthZenBoxcarRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); + + AdjustBoxcarRequestSerialization(deserializedRequest); + + deserializedRequest.Should().BeEquivalentTo(evaluationRequest.Body.ToDto()); + } + + [Theory] + [InlineData("true", Decision.Permit)] + [InlineData("false", Decision.Deny)] + public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldParseDecisionCorrectly(string jsonDecision, Decision expectedDecision) + { + string response = + $$""" + { + "evaluations": [ + { + "decision": {{jsonDecision}} + }, + { + "decision": {{jsonDecision}} + } + ] + } + """; + + List sentRequests = new List(); + + int calls = 0; + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + { + Body = new AuthZenBoxcarEvaluationBody() + { + Evaluations = new List() + { + new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + } + } + }; + + AuthZenBoxcarResponse authZenResponse = await sut.Evaluate(evaluationRequest); + + authZenResponse.Evaluations.Should().BeEquivalentTo(new List() + { new () { Decision = expectedDecision, @@ -820,11 +1358,32 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldExt } """; + + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(response) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); }); var sut = CreateSut(); @@ -855,10 +1414,30 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaults_ShouldExt [Fact] public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRequestFails_ShouldThrowAuthZenRequestFailureException() { + + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + }); var sut = CreateSut(); @@ -899,12 +1478,33 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRespons } """; + + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { Content = new StringContent(response), Headers = { { "X-Request-ID", expectedRequestId } } + }); }); var sut = CreateSut(); @@ -913,11 +1513,9 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRespons { Body = new AuthZenBoxcarEvaluationBody() { - - Evaluations = new List() { - new AuthZenEvaluationBody() + new() { Subject = new AuthZenSubject { @@ -937,13 +1535,31 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsNoDefaultsAndRespons [Fact] public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_ShouldPostToCorrectEndpoint() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleBoxcarResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); @@ -985,22 +1601,40 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh await sut.Evaluate(evaluationRequest); - requestSent.Should().NotBeNull(); - requestSent.Method.Should().Be(HttpMethod.Post); - requestSent.RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.UriBase}/{AuthZenClient.BoxcarUri}"); - requestSent.Content.Headers.ContentType.MediaType.Should().Be("application/json"); + sentRequests[1].Should().NotBeNull(); + sentRequests[1].Method.Should().Be(HttpMethod.Post); + sentRequests[1].RequestUri.Should().Be(metadataEvaluationsEndpoint); + sentRequests[1].Content.Headers.ContentType.MediaType.Should().Be("application/json"); } [Fact] public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWithCorrelationId_ShouldAddRequestIdHeaderToRequest() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleBoxcarResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); @@ -1043,7 +1677,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit await sut.Evaluate(evaluationRequest); - requestSent.Headers + sentRequests[1].Headers .Should() .ContainSingle(h => h.Key == "X-Request-ID" && h.Value.Contains(evaluationRequest.CorrelationId)); @@ -1052,13 +1686,31 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesWit [Fact] public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_ShouldPostSerializedRequestCorrectly() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleBoxcarResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); @@ -1101,7 +1753,7 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh await sut.Evaluate(evaluationRequest); - string sentContent = await requestSent.Content.ReadAsStringAsync(); + string sentContent = await sentRequests[1].Content.ReadAsStringAsync(); AuthZenBoxcarRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); @@ -1129,11 +1781,33 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh ] } """; + + + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => { - Content = new StringContent(response) + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); }); var sut = CreateSut(); @@ -1217,11 +1891,32 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh } """; + + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(response) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(response) + }); }); var sut = CreateSut(); @@ -1270,10 +1965,30 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValues_Sh public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAndRequestFails_ShouldThrowAuthZenRequestFailureException() { + + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + }); var sut = CreateSut(); @@ -1331,12 +2046,32 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd } """; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { Content = new StringContent(response), Headers = { { "X-Request-ID", expectedRequestId } } + }); }); var sut = CreateSut(); @@ -1387,13 +2122,31 @@ public async Task Evaluate_WhenCalledWithMultipleEvaluationsWithDefaultValuesAnd [InlineData(BoxcarSemantics.PermitOnFirstPermit)] public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarSemantics semantics) { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleBoxcarResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }); }); var sut = CreateSut(); @@ -1440,7 +2193,7 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS await sut.Evaluate(evaluationRequest); - string sentContent = await requestSent.Content.ReadAsStringAsync(); + string sentContent = await sentRequests[1].Content.ReadAsStringAsync(); AuthZenBoxcarRequestMessageDto deserializedRequest = JsonSerializer.Deserialize(sentContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); @@ -1454,18 +2207,36 @@ public async Task Evaluate_WhenBoxCarOptions_ShouldApplyOptionsCorrectly(BoxcarS [Fact] public async Task Evaluate_WhenBoxCarEvaluationsIsMissing_ShouldFallbackToSingleEvaluation() { - HttpRequestMessage requestSent = null; + List sentRequests = new List(); + + int calls = 0; httpMessageHandler.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .Callback((r, c) => requestSent = r) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => { - Content = new StringContent(simpleEvaluationResponse) + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }); }); var sut = CreateSut(); - var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + var evaluationRequest = new AuthZenBoxcarEvaluationRequest { Body = new AuthZenBoxcarEvaluationBody() { @@ -1492,9 +2263,563 @@ public async Task Evaluate_WhenBoxCarEvaluationsIsMissing_ShouldFallbackToSingle await sut.Evaluate(evaluationRequest); + sentRequests[1].Should().NotBeNull(); + sentRequests[1].Method.Should().Be(HttpMethod.Post); + sentRequests[1].RequestUri.Should().Be(metadataEvaluationEndpoint); + sentRequests[1].Content.Headers.ContentType.MediaType.Should().Be("application/json"); + } + + [Fact] + public async Task GetMetadata_WhenCalled_ShouldSendGetToCorrectEndpoint() + { + var metadataResponse = new AuthZenMetadataResponse + { + PolicyDecisionPoint = "asldkfj", + AccessEvaluationEndpoint = "asldkfj", + AccessEvaluationsEndpoint = "asldkfj", + }; + + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(metadataResponse, new JsonSerializerOptions(){ PropertyNamingPolicy = JsonNamingPolicy.CamelCase})) + }); + + var sut = CreateSut(); + + await sut.GetMetadata(); + requestSent.Should().NotBeNull(); - requestSent.Method.Should().Be(HttpMethod.Post); - requestSent.RequestUri.Should().Be($"{optionsValue.AuthorizationUrl}/{AuthZenClient.UriBase}/{AuthZenClient.EvaluationUri}"); - requestSent.Content.Headers.ContentType.MediaType.Should().Be("application/json"); + requestSent.Method.Should().Be(HttpMethod.Get); + requestSent.RequestUri.Should().Be($"https://localhost:5001/.well-known/authzen-configuration" ); + } + + [Theory] + [InlineData(400)] + [InlineData(401)] + [InlineData(403)] + [InlineData(404)] + [InlineData(500)] + [InlineData(501)] + [InlineData(502)] + [InlineData(503)] + public async Task GetMetadata_WhenResponseIsNotOKStatusCode_ShouldThrowAuthZenRequestFailure(int code) + { + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage((HttpStatusCode)code)); + + var sut = CreateSut(); + + var act = async () => await sut.GetMetadata(); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task GetMetadata_WhenResponseIsOk_ShouldDeserializeAndReturnResponse() + { + var metadataResponse = new AuthZenMetadataResponse + { + PolicyDecisionPoint = "asldkfj", + AccessEvaluationEndpoint = "asldkfj", + AccessEvaluationsEndpoint = "asldkfj", + }; + + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(metadataResponse, new JsonSerializerOptions(){ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower})) + }); + + var sut = CreateSut(); + + var actualResponse = await sut.GetMetadata(); + + actualResponse.Should().BeEquivalentTo(metadataResponse); + } + + [Fact] + public async Task GetMetadata_WhenResponseIncludesAllProperties_ShouldDeserializeFully() + { + var metadataResponse = new AuthZenMetadataResponse + { + PolicyDecisionPoint = "hjsbegrlhjsbdfg", + AccessEvaluationEndpoint = "klefn;aejng", + AccessEvaluationsEndpoint = "kjrgnljkrgbfg", + SearchActionEndpoint = "lhjrgblhzjsb", + SearchResourceEndpoint = "fdjhgbdljghbsdlg", + SearchSubjectEndpoint = "hjdrbglsdjhgbsld" + }; + + HttpRequestMessage requestSent = null; + httpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => requestSent = r) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonSerializer.Serialize(metadataResponse, new JsonSerializerOptions(){ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower})) + }); + + var sut = CreateSut(); + + var actualResponse = await sut.GetMetadata(); + + actualResponse.Should().BeEquivalentTo(metadataResponse); + } + + [Fact] + public async Task Evaluation_On404ForFirstEvaluation_ShouldThrowAuthZenRequestFailureException() + { + List sentRequests = new List(); + + int calls = 0; + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenEvaluationRequest + { + Body = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }; + + var act = async () => await sut.Evaluate(evaluationRequest); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task Evaluation_On404WithCachedMetadata_ShouldGetMetadataAgainAndRetry() + { + List sentRequests = new List(); + + int calls = 0; + + List> responseSequence = new List> + { + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + ), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }), + }; + + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + sentRequests.Add(r); + }) + .Returns(() => + { + return responseSequence[calls++]; + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenEvaluationRequest + { + Body = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }; + + await sut.Evaluate(evaluationRequest); + await sut.Evaluate(evaluationRequest); + + calls.Should().Be(5); + + IsMetadataRequest(sentRequests[0]).Should().BeTrue(); + IsEvaluationRequest(sentRequests[1]).Should().BeTrue(); + IsEvaluationRequest(sentRequests[2]).Should().BeTrue(); + IsMetadataRequest(sentRequests[3]).Should().BeTrue(); + IsEvaluationRequest(sentRequests[4]).Should().BeTrue(); + } + + private bool IsMetadataRequest(HttpRequestMessage request) + { + return request.Method == HttpMethod.Get && request.RequestUri.ToString() == ($"{optionsValue.AuthorizationUrl}/{AuthZenClient.MetadataEndpointUri}"); + } + + private bool IsEvaluationRequest(HttpRequestMessage request) + { + return request.Method == HttpMethod.Post && request.RequestUri.ToString() == metadataEvaluationEndpoint; + } + + private bool IsBoxcarRequest(HttpRequestMessage request) + { + return request.Method == HttpMethod.Post && request.RequestUri.ToString() == metadataEvaluationsEndpoint; + } + + [Fact] + public async Task Evaluation_On404AfterRecheckingMetadata_ShouldThrowAuthZenRequestFailureException() + { + List sentRequests = new List(); + + int calls = 0; + + List> responseSequence = new List> + { + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleEvaluationResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + ), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + ), + }; + + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + sentRequests.Add(r); + }) + .Returns(() => + { + return responseSequence[calls++]; + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenEvaluationRequest + { + Body = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }; + + await sut.Evaluate(evaluationRequest); + var act = async () => await sut.Evaluate(evaluationRequest); + + act.Should().ThrowAsync(); + + calls.Should().Be(5); + + IsMetadataRequest(sentRequests[0]).Should().BeTrue(); + IsEvaluationRequest(sentRequests[1]).Should().BeTrue(); + IsEvaluationRequest(sentRequests[2]).Should().BeTrue(); + IsMetadataRequest(sentRequests[3]).Should().BeTrue(); + IsEvaluationRequest(sentRequests[4]).Should().BeTrue(); + } + + [Fact] + public async Task BoxcarEvaluation_On404ForFirstEvaluation_ShouldThrowAuthZenRequestFailureException() + { + List sentRequests = new List(); + + int calls = 0; + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + calls++; + sentRequests.Add(r); + }) + .Returns(() => + { + if (calls == 1) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + { + Body = new AuthZenBoxcarEvaluationBody() + { + Evaluations = new List() + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + } + }, + CorrelationId = Guid.NewGuid().ToString(), + }; + + var act = async () => await sut.Evaluate(evaluationRequest); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task BoxcarEvaluation_On404WithCachedMetadata_ShouldGetMetadataAgainAndRetry() + { + List sentRequests = new List(); + + int calls = 0; + + List> responseSequence = new List> + { + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + ), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }), + }; + + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + sentRequests.Add(r); + }) + .Returns(() => + { + return responseSequence[calls++]; + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + { + Body = new AuthZenBoxcarEvaluationBody() + { + Evaluations = new List() + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + } + }, + CorrelationId = Guid.NewGuid().ToString(), + }; + + await sut.Evaluate(evaluationRequest); + await sut.Evaluate(evaluationRequest); + + calls.Should().Be(5); + + IsMetadataRequest(sentRequests[0]).Should().BeTrue(); + IsBoxcarRequest(sentRequests[1]).Should().BeTrue(); + IsBoxcarRequest(sentRequests[2]).Should().BeTrue(); + IsMetadataRequest(sentRequests[3]).Should().BeTrue(); + IsBoxcarRequest(sentRequests[4]).Should().BeTrue(); + } + + [Fact] + public async Task BoxcarEvaluation_On404AfterRecheckingMetadata_ShouldThrowAuthZenRequestFailureException() + { + List sentRequests = new List(); + + int calls = 0; + + List> responseSequence = new List> + { + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleBoxcarResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + ), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(simpleMetadataResponse) + }), + Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + ), + }; + + httpMessageHandler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((r, c) => + { + sentRequests.Add(r); + }) + .Returns(() => + { + return responseSequence[calls++]; + }); + + var sut = CreateSut(); + + var evaluationRequest = new AuthZenBoxcarEvaluationRequest() + { + Body = new AuthZenBoxcarEvaluationBody() + { + Evaluations = new List() + { + new() + { + Subject = new AuthZenSubject + { + Id = "dasfgthb", + Type = "aerfbqret" + } + } + }, + DefaultValues = new AuthZenEvaluationBody() + { + Subject = new AuthZenSubject() + { + Id = "jk;dfgn", + Type = "jk;dfbgn;" + }, + Resource = new AuthZenResource() + { + Type = "hjldfbg", + Id = "jkldfbgns" + }, + Action = new AuthZenAction() + { + Name = "hjkldfgb" + } + } + }, + CorrelationId = Guid.NewGuid().ToString(), + }; + + await sut.Evaluate(evaluationRequest); + var act = async () => await sut.Evaluate(evaluationRequest); + + act.Should().ThrowAsync(); + + calls.Should().Be(5); + + IsMetadataRequest(sentRequests[0]).Should().BeTrue(); + IsBoxcarRequest(sentRequests[1]).Should().BeTrue(); + IsBoxcarRequest(sentRequests[2]).Should().BeTrue(); + IsMetadataRequest(sentRequests[3]).Should().BeTrue(); + IsBoxcarRequest(sentRequests[4]).Should().BeTrue(); } } diff --git a/src/CSharp/Rsk.AuthZen.Client/AuthZenClient.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenClient.cs index 154fbf4..897177a 100644 --- a/src/CSharp/Rsk.AuthZen.Client/AuthZenClient.cs +++ b/src/CSharp/Rsk.AuthZen.Client/AuthZenClient.cs @@ -29,9 +29,11 @@ public class AuthZenClient : IAuthZenClient internal const string UriBase = "access/v1"; internal const string EvaluationUri = "evaluation"; internal const string BoxcarUri = "evaluations"; + internal const string MetadataEndpointUri = ".well-known/authzen-configuration"; private const string RequestIdHeader = "X-Request-ID"; private readonly HttpClient httpClient; + private AuthZenMetadataResponse _metadata; private static JsonSerializerOptions serializerOptions = new JsonSerializerOptions { @@ -56,10 +58,39 @@ public AuthZenClient(IHttpClientFactory httpClientFactory, IOptions + public async Task GetMetadata() + { + var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{MetadataEndpointUri}"); + + HttpResponseMessage response = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + throw new AuthZenRequestFailureException($"Metadata request failed with status code: {response.StatusCode}"); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + + return JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions(){ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower}); + } + /// public async Task Evaluate(AuthZenEvaluationRequest request) { - var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{EvaluationUri}", UriKind.Relative)); + bool canRetry404 = true; + if (_metadata == null) + { + _metadata = await GetMetadata(); + canRetry404 = false; + } + + return await EvaluateInternal(request, canRetry404); + } + + private async Task EvaluateInternal(AuthZenEvaluationRequest request, bool canRetry) + { + var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(_metadata.AccessEvaluationEndpoint, UriKind.Absolute)); if (request.CorrelationId != null) { requestMessage.Headers.Add(RequestIdHeader, request.CorrelationId); @@ -72,6 +103,12 @@ public async Task Evaluate(AuthZenEvaluationRequest request) HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + if (canRetry && responseMessage.StatusCode == System.Net.HttpStatusCode.NotFound) + { + _metadata = null; + return await Evaluate(request); + } + if (!responseMessage.IsSuccessStatusCode) { throw new AuthZenRequestFailureException($"Evaluation request failed with status code: {responseMessage.StatusCode}"); @@ -96,13 +133,30 @@ public async Task Evaluate(AuthZenEvaluationRequest request) /// public async Task Evaluate(AuthZenBoxcarEvaluationRequest request) + { + bool canRetry404 = true; + if (_metadata == null) + { + _metadata = await GetMetadata(); + canRetry404 = false; + } + + return await BoxcarEvaluateInternal(request, canRetry404); + } + + private async Task BoxcarEvaluateInternal(AuthZenBoxcarEvaluationRequest request, bool canRetry) { if (IsMultiEvaluationsMissing(request)) { return await FallbackToSingleEvaluation(request); } - var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{UriBase}/{BoxcarUri}", UriKind.Relative)); + if (string.IsNullOrWhiteSpace(_metadata.AccessEvaluationsEndpoint)) + { + throw new NotSupportedException("The AuthZen server does not support boxcar evaluation requests."); + } + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(_metadata.AccessEvaluationsEndpoint, UriKind.Absolute)); if (request.CorrelationId != null) { @@ -116,6 +170,12 @@ public async Task Evaluate(AuthZenBoxcarEvaluationRequest HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + if (canRetry && responseMessage.StatusCode == System.Net.HttpStatusCode.NotFound) + { + _metadata = null; + return await Evaluate(request); + } + if (!responseMessage.IsSuccessStatusCode) { throw new AuthZenRequestFailureException($"Evaluation request failed with status code: {responseMessage.StatusCode}"); diff --git a/src/CSharp/Rsk.AuthZen.Client/AuthZenMetadataResponse.cs b/src/CSharp/Rsk.AuthZen.Client/AuthZenMetadataResponse.cs new file mode 100644 index 0000000..fb0d6a9 --- /dev/null +++ b/src/CSharp/Rsk.AuthZen.Client/AuthZenMetadataResponse.cs @@ -0,0 +1,17 @@ +namespace Rsk.AuthZen.Client +{ + public class AuthZenMetadataResponse + { + public string PolicyDecisionPoint { get; set; } = null!; + + public string AccessEvaluationEndpoint { get; set; } = null!; + + public string AccessEvaluationsEndpoint { get; set; } + + public string SearchSubjectEndpoint { get; set; } + + public string SearchActionEndpoint { get; set; } + + public string SearchResourceEndpoint { get; set; } + } +} \ No newline at end of file diff --git a/src/CSharp/Rsk.AuthZen.Client/IAuthZenClient.cs b/src/CSharp/Rsk.AuthZen.Client/IAuthZenClient.cs index e5aff4a..4fe0d40 100644 --- a/src/CSharp/Rsk.AuthZen.Client/IAuthZenClient.cs +++ b/src/CSharp/Rsk.AuthZen.Client/IAuthZenClient.cs @@ -8,6 +8,14 @@ namespace Rsk.AuthZen.Client /// public interface IAuthZenClient { + /// + /// Gets the metadata information of the various endpoints from the AuthZen service. + /// + /// + /// A task that represents the asynchronous operation. The task result contains the metadata response. + /// + Task GetMetadata(); + /// /// Evaluates a single authorization request. /// diff --git a/src/CSharp/Rsk.AuthZen.sln.DotSettings.user b/src/CSharp/Rsk.AuthZen.sln.DotSettings.user index 9f144f1..7791396 100644 --- a/src/CSharp/Rsk.AuthZen.sln.DotSettings.user +++ b/src/CSharp/Rsk.AuthZen.sln.DotSettings.user @@ -1,7 +1,13 @@  + ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded <SessionState ContinuousTestingMode="0" IsActive="True" Name="AuthZenClientTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> </SessionState> \ No newline at end of file From 1e4a47393bd7e490a935342369d3c5d7361e6a81 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Fri, 29 Aug 2025 14:26:08 +0100 Subject: [PATCH 10/33] Add Typscript client --- src/Typescript/README.md | 307 ++ src/Typescript/examples/basic-usage.ts | 272 ++ src/Typescript/jest.config.js | 18 + src/Typescript/package-lock.json | 6123 ++++++++++++++++++++++++ src/Typescript/package.json | 45 + src/Typescript/src/client.test.ts | 1044 ++++ src/Typescript/src/client.ts | 500 ++ src/Typescript/src/discovery.test.ts | 565 +++ src/Typescript/src/index.ts | 26 + src/Typescript/src/types.ts | 300 ++ src/Typescript/test-harness/index.html | 1016 ++++ src/Typescript/tsconfig.json | 32 + src/Typescript/webpack.config.js | 26 + 13 files changed, 10274 insertions(+) create mode 100644 src/Typescript/README.md create mode 100644 src/Typescript/examples/basic-usage.ts create mode 100644 src/Typescript/jest.config.js create mode 100644 src/Typescript/package-lock.json create mode 100644 src/Typescript/package.json create mode 100644 src/Typescript/src/client.test.ts create mode 100644 src/Typescript/src/client.ts create mode 100644 src/Typescript/src/discovery.test.ts create mode 100644 src/Typescript/src/index.ts create mode 100644 src/Typescript/src/types.ts create mode 100644 src/Typescript/test-harness/index.html create mode 100644 src/Typescript/tsconfig.json create mode 100644 src/Typescript/webpack.config.js diff --git a/src/Typescript/README.md b/src/Typescript/README.md new file mode 100644 index 0000000..3feee9e --- /dev/null +++ b/src/Typescript/README.md @@ -0,0 +1,307 @@ +# AuthZen TypeScript Client + +A comprehensive TypeScript client library for interacting with [AuthZen](https://openid.github.io/authzen/)-compliant Policy Decision Points (PDPs). This library implements the AuthZen Authorization API 1.0 specification. + +## Features + +- ✅ **Access Evaluation API** - Single authorization decisions +- ✅ **Access Evaluations API** - Batch authorization decisions with multiple evaluation semantics +- 🔄 **Search APIs** - Subject, Resource, and Action search (coming soon) +- 🛡️ **Type Safety** - Full TypeScript support with comprehensive type definitions +- 🚀 **Modern** - Built with ES2020, supports both Node.js and browser environments +- 🔧 **Flexible** - Configurable fetch implementation, timeouts, and custom headers +- 📝 **Well Documented** - Comprehensive JSDoc comments and examples + +## Installation + +```bash +npm install authzen-client +``` + +For Node.js environments, you'll also need to install node-fetch: + +```bash +npm install node-fetch +npm install --save-dev @types/node-fetch +``` + +## Quick Start + +```typescript +import { AuthZenClient, createSubject, createAction, createResource } from 'authzen-client'; + +// Create a client +const client = new AuthZenClient({ + baseUrl: 'https://pdp.mycompany.com', + token: 'your-bearer-token', // Optional +}); + +// Simple access evaluation +const response = await client.evaluate({ + subject: createSubject('user', 'alice@example.com'), + action: createAction('can_read'), + resource: createResource('document', '123'), +}); + +if (response.decision) { + console.log('✅ Access granted'); +} else { + console.log('❌ Access denied'); +} +``` + +## API Reference + +### Client Configuration + +```typescript +interface AuthZenClientConfig { + baseUrl: string; // PDP base URL (required) + apiVersion?: string; // API version (default: 'v1') + token?: string; // Bearer token for authentication + headers?: Record; // Custom headers + timeout?: number; // Request timeout in ms (default: 30000) + fetch?: Function; // Custom fetch implementation +} +``` + +### Single Access Evaluation + +Evaluate a single authorization request: + +```typescript +const response = await client.evaluate({ + subject: { + type: 'user', + id: 'alice@example.com', + properties: { department: 'Sales' } + }, + action: { + name: 'can_read', + properties: { method: 'GET' } + }, + resource: { + type: 'document', + id: '123', + properties: { classification: 'confidential' } + }, + context: { + time: '2024-01-01T12:00:00Z', + location: 'office' + } +}); +``` + +### Batch Access Evaluations + +Evaluate multiple authorization requests in a single call: + +```typescript +const response = await client.evaluateBatch({ + // Default values applied to all evaluations + subject: createSubject('user', 'alice@example.com'), + context: { time: new Date().toISOString() }, + + // Individual evaluations + evaluations: [ + { + action: createAction('can_read'), + resource: createResource('document', '123') + }, + { + action: createAction('can_write'), + resource: createResource('document', '456') + } + ], + + options: { + evaluations_semantic: 'execute_all' // or 'deny_on_first_deny' or 'permit_on_first_permit' + } +}); +``` + +### Evaluation Semantics + +The batch evaluation API supports three evaluation semantics: + +- **`execute_all`** (default) - Execute all evaluations and return all results +- **`deny_on_first_deny`** - Stop and return on the first denial (short-circuit AND) +- **`permit_on_first_permit`** - Stop and return on the first permit (short-circuit OR) + +## Utility Functions + +The library provides helpful utility functions for creating AuthZen objects: + +```typescript +import { + createSubject, + createResource, + createAction, + createContext, + createContextWithTime, + SubjectTypes, + ResourceTypes, + ActionNames +} from 'authzen-client'; + +// Create objects with utilities +const user = createSubject(SubjectTypes.USER, 'alice@example.com'); +const document = createResource(ResourceTypes.DOCUMENT, '123'); +const readAction = createAction(ActionNames.READ); +const context = createContextWithTime({ location: 'office' }); +``` + +### Built-in Constants + +```typescript +// Subject types +SubjectTypes.USER // 'user' +SubjectTypes.SERVICE // 'service' +SubjectTypes.GROUP // 'group' + +// Resource types +ResourceTypes.DOCUMENT // 'document' +ResourceTypes.API // 'api' +ResourceTypes.FOLDER // 'folder' + +// Action names +ActionNames.READ // 'can_read' +ActionNames.WRITE // 'can_write' +ActionNames.DELETE // 'can_delete' +``` + +## Error Handling + +The client throws `AuthZenError` for API-related errors: + +```typescript +import { AuthZenError } from 'authzen-client'; + +try { + const response = await client.evaluate(request); +} catch (error) { + if (error instanceof AuthZenError) { + console.error('Status:', error.status); + console.error('Message:', error.message); + console.error('Request ID:', error.requestId); + } +} +``` + +## Node.js Usage + +For Node.js environments, provide a fetch implementation: + +```typescript +import fetch from 'node-fetch'; +import { AuthZenClient } from 'authzen-client'; + +const client = new AuthZenClient({ + baseUrl: 'https://pdp.mycompany.com', + fetch: fetch as any, // Provide fetch implementation +}); +``` + +## Browser Usage + +In browser environments, the global `fetch` API is used automatically: + +```typescript +import { AuthZenClient } from 'authzen-client'; + +const client = new AuthZenClient({ + baseUrl: 'https://pdp.mycompany.com', + // No fetch needed - uses browser's global fetch +}); +``` + +## Advanced Examples + +### Complex Authorization with Rich Context + +```typescript +const response = await client.evaluate({ + subject: createSubject('user', 'alice@example.com', { + department: 'Sales', + role: 'Manager', + clearance_level: 'confidential' + }), + action: createAction('can_read', { + method: 'GET', + api_endpoint: '/documents/123' + }), + resource: createResource('document', '123', { + owner: 'bob@example.com', + classification: 'confidential', + project: 'Project Alpha' + }), + context: createContextWithTime({ + location: 'office', + device_type: 'laptop', + ip_address: '192.168.1.100' + }) +}); +``` + +### Batch Evaluation with Short-Circuit Logic + +```typescript +const response = await client.evaluateBatch({ + subject: createSubject('user', 'alice@example.com'), + evaluations: [ + { action: createAction('can_read'), resource: createResource('document', '1') }, + { action: createAction('can_read'), resource: createResource('document', '2') }, + { action: createAction('can_read'), resource: createResource('document', '3') } + ], + options: { + evaluations_semantic: 'deny_on_first_deny' // Stop on first denial + } +}); + +console.log(`Evaluated ${response.evaluations.length} requests`); +``` + +## Development + +### Building + +```bash +npm run build +``` + +### Testing + +```bash +npm test +``` + +### Linting + +```bash +npm run lint +``` + +## Contributing + +Contributions are welcome! Please read our contributing guidelines and submit pull requests for any improvements. + +## License + +MIT License - see LICENSE file for details. + +## Related + +- [AuthZen Specification](https://openid.github.io/authzen/) +- [OpenID Foundation](https://openid.net/) +- [Policy Decision Points (PDPs)](https://en.wikipedia.org/wiki/XACML#Policy_Decision_Point_(PDP)) + +## Changelog + +### 1.0.0 + +- Initial release +- Support for Access Evaluation API +- Support for Access Evaluations API (batch) +- TypeScript support +- Utility functions +- Comprehensive documentation diff --git a/src/Typescript/examples/basic-usage.ts b/src/Typescript/examples/basic-usage.ts new file mode 100644 index 0000000..c44ed92 --- /dev/null +++ b/src/Typescript/examples/basic-usage.ts @@ -0,0 +1,272 @@ +import { + AuthZenClient, + AuthZenError, + AuthZenValidationError, + AuthZenRequestError, + AuthZenResponseError, + AuthZenNetworkError, + AuthZenDiscoveryError, + AccessEvaluationRequest, + AccessEvaluationsRequest, + Subject, + Resource, + Action, + Context, +} from '../src'; + +/** + * Discovery endpoint example + */ +async function discoveryExample(): Promise { + console.log('\n=== Discovery Example ==='); + + const client = new AuthZenClient({ + pdpUrl: 'https://pdp.mycompany.com', + token: 'your-bearer-token-here', + }); + + try { + const config = await client.discover(); + + console.log('✅ Discovery successful!'); + console.log(`Policy Decision Point: ${config.policy_decision_point}`); + + if (config.access_evaluation_endpoint) { + console.log(`Single Evaluation Endpoint: ${config.access_evaluation_endpoint}`); + } + + if (config.access_evaluations_endpoint) { + console.log(`Batch Evaluations Endpoint: ${config.access_evaluations_endpoint}`); + } + + if (config.search_subject_endpoint) { + console.log(`Subject Search Endpoint: ${config.search_subject_endpoint}`); + } + + if (config.search_resource_endpoint) { + console.log(`Resource Search Endpoint: ${config.search_resource_endpoint}`); + } + + if (config.search_action_endpoint) { + console.log(`Action Search Endpoint: ${config.search_action_endpoint}`); + } + + console.log('\nFull Configuration:'); + console.log(JSON.stringify(config, null, 2)); + + } catch (error) { + if (error instanceof AuthZenDiscoveryError) { + console.error('❌ Discovery configuration error:', error.message); + } else if (error instanceof AuthZenRequestError) { + console.error('❌ Discovery request failed:', error.message, `(Status: ${error.statusCode})`); + } else if (error instanceof AuthZenNetworkError) { + console.error('❌ Discovery network error:', error.message); + } else { + console.error('❌ Discovery failed:', error); + } + } +} + +/** + * Single evaluation example + */ +async function singleEvaluationExample(): Promise { + console.log('\n=== Single Evaluation Example ==='); + + const client = new AuthZenClient({ + pdpUrl: 'https://pdp.mycompany.com', + token: 'your-bearer-token-here', + timeout: 5000, + }); + + try { + const request: AccessEvaluationRequest = { + subject: { + type: 'user', + id: 'alice@company.com', + properties: { + role: 'employee', + department: 'engineering', + clearance_level: 2 + } + }, + resource: { + type: 'document', + id: 'design-doc-123', + properties: { + classification: 'internal', + owner: 'alice@company.com', + project: 'authzen-client' + } + }, + action: { + name: 'read', + properties: { + method: 'GET', + requested_fields: ['title', 'content'] + } + }, + context: { + ip_address: '192.168.1.100', + time: new Date().toISOString(), + environment: 'production', + user_agent: 'AuthZen-Client/1.0', + request_id: 'req-12345' + } + }; + + const response = await client.evaluate(request); + + console.log('✅ Single evaluation successful!'); + console.log(`Decision: ${response.decision ? 'ALLOW' : 'DENY'}`); + + if (response.context) { + console.log('Response context:'); + console.log(JSON.stringify(response.context, null, 2)); + } + + } catch (error) { + if (error instanceof AuthZenValidationError) { + console.error('❌ Validation error:', error.message); + } else if (error instanceof AuthZenRequestError) { + console.error('❌ Request failed:', error.message, `(Status: ${error.statusCode})`); + } else if (error instanceof AuthZenNetworkError) { + console.error('❌ Network error:', error.message); + } else { + console.error('❌ Evaluation failed:', error); + } + } +} + +/** + * Batch evaluations example + */ +async function batchEvaluationsExample(): Promise { + console.log('\n=== Batch Evaluations Example ==='); + + const client = new AuthZenClient({ + pdpUrl: 'https://pdp.mycompany.com', + token: 'your-bearer-token-here', + }); + + try { + const request: AccessEvaluationsRequest = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@company.com' }, + resource: { type: 'document', id: 'doc-1' }, + action: { name: 'read' } + }, + { + // Missing subject - will use default + resource: { type: 'document', id: 'doc-2' }, + action: { name: 'write' } + }, + { + subject: { type: 'user', id: 'charlie@company.com' }, + // Missing resource - will use default + action: { name: 'delete' } + }, + { + subject: { type: 'user', id: 'david@company.com' }, + resource: { type: 'api', id: 'user-service' }, + // Missing action - will use default + } + ], + // Default values applied when missing from individual evaluations + subject: { + type: 'user', + id: 'default-user@company.com', + properties: { role: 'guest' } + }, + resource: { + type: 'document', + id: 'shared-document', + properties: { classification: 'public' } + }, + action: { + name: 'read', + properties: { method: 'GET' } + }, + context: { + environment: 'production', + ip_address: '10.0.0.1', + time: new Date().toISOString(), + batch_id: 'batch-789' + }, + options: { + evaluations_semantic: 'execute_all' + } + }; + + const response = await client.evaluations(request); + + console.log('✅ Batch evaluations successful!'); + console.log(`Processed ${response.evaluations.length} evaluations:`); + + response.evaluations.forEach((evaluation, index) => { + console.log(` ${index + 1}. Decision: ${evaluation.decision ? 'ALLOW' : 'DENY'}`); + if (evaluation.context?.reason_admin) { + console.log(` Reason: ${evaluation.context.reason_admin}`); + } + }); + + } catch (error) { + if (error instanceof AuthZenValidationError) { + console.error('❌ Validation error:', error.message); + } else if (error instanceof AuthZenRequestError) { + console.error('❌ Request failed:', error.message, `(Status: ${error.statusCode})`); + } else if (error instanceof AuthZenNetworkError) { + console.error('❌ Network error:', error.message); + } else { + console.error('❌ Batch evaluations failed:', error); + } + } +} + +/** + * Run all basic examples + */ +async function runBasicExamples(): Promise { + console.log('🚀 AuthZen TypeScript Client - Basic Usage Examples'); + console.log('=================================================='); + + await discoveryExample(); + await singleEvaluationExample(); + await batchEvaluationsExample(); + + console.log('\n✅ All basic examples completed!'); +} + +// Export examples +export { + discoveryExample, + singleEvaluationExample, + batchEvaluationsExample, + runBasicExamples, +}; + +// Helper functions for creating common objects +export function createUserSubject(id: string, properties?: Record): Subject { + return { type: 'user', id, ...(properties && { properties }) }; +} + +export function createDocumentResource(id: string, properties?: Record): Resource { + return { type: 'document', id, ...(properties && { properties }) }; +} + +export function createAction(name: string, properties?: Record): Action { + return { name, ...(properties && { properties }) }; +} + +export function createContextWithTimestamp(additionalContext?: Record): Context { + return { + time: new Date().toISOString(), + ...additionalContext, + }; +} + +// Run examples if this file is executed directly +if (require.main === module) { + runBasicExamples().catch(console.error); +} diff --git a/src/Typescript/jest.config.js b/src/Typescript/jest.config.js new file mode 100644 index 0000000..e2d32e2 --- /dev/null +++ b/src/Typescript/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.ts', '**/*.(test|spec).ts'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.test.ts', + '!src/**/*.d.ts', + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleNameMapping: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, +}; diff --git a/src/Typescript/package-lock.json b/src/Typescript/package-lock.json new file mode 100644 index 0000000..f6a6ba7 --- /dev/null +++ b/src/Typescript/package-lock.json @@ -0,0 +1,6123 @@ +{ + "name": "authzen-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "authzen-client", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.4", + "ts-loader": "^9.5.2", + "typescript": "^5.5.4", + "webpack": "^5.101.1", + "webpack-cli": "^6.0.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "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==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@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", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.199", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", + "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "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/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.101.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.1.tgz", + "integrity": "sha512-rHY3vHXRbkSfhG6fH8zYQdth/BtDgXXuR2pHF++1f/EBkI8zkgM5XWfsC3BvOoW9pr1CvZ1qQCxhCEsbNgT50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/Typescript/package.json b/src/Typescript/package.json new file mode 100644 index 0000000..d849517 --- /dev/null +++ b/src/Typescript/package.json @@ -0,0 +1,45 @@ +{ + "name": "authzen-client", + "version": "1.0.0", + "description": "TypeScript client library for AuthZen Authorization API", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "webpack", + "build:watch": "webpack --watch", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint src/**/*.ts", + "lint:fix": "eslint src/**/*.ts --fix", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "authzen", + "authorization", + "access-control", + "security", + "typescript" + ], + "author": "Your Name", + "license": "MIT", + "devDependencies": { + "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.4", + "ts-loader": "^9.5.2", + "typescript": "^5.5.4", + "webpack": "^5.101.1", + "webpack-cli": "^6.0.1" + }, + "files": [ + "dist/**/*", + "README.md" + ], + "repository": { + "type": "git", + "url": "https://github.com/yourusername/authzen-client.git" + } +} diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts new file mode 100644 index 0000000..5bcf075 --- /dev/null +++ b/src/Typescript/src/client.test.ts @@ -0,0 +1,1044 @@ +import { AuthZenClient } from './client'; +import { + AuthZenError, + AuthZenRequestError, + AuthZenResponseError, + AuthZenNetworkError, + AuthZenValidationError, + AccessEvaluationRequest, + AccessEvaluationsRequest, +} from './types'; + +// Mock global fetch +const mockFetch = jest.fn(); +global.fetch = mockFetch; + +describe('AuthZenClient', () => { + beforeEach(() => { + mockFetch.mockClear(); + jest.clearAllTimers(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('constructor', () => { + it('should create a client with required config', () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + expect(client).toBeInstanceOf(AuthZenClient); + expect(client.pdpUrl).toBe('https://example.com'); + }); + + it('should normalize PDP URL by removing trailing slash', () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com/', + }); + + expect(client.pdpUrl).toBe('https://example.com'); + }); + + it('should set default timeout to 10 seconds', () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + // We can't directly access timeout, but we can test it through behavior + expect(client).toBeInstanceOf(AuthZenClient); + + // Todo + }); + + it('should set custom timeout when provided', () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + timeout: 5000, + }); + + expect(client).toBeInstanceOf(AuthZenClient); + + // Todo + }); + + it('should set Authorization header when token provided', () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + expect(client).toBeInstanceOf(AuthZenClient); + + // Todo + }); + + it('should merge custom headers', () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + headers: { 'Custom-Header': 'test-value' }, + }); + + expect(client).toBeInstanceOf(AuthZenClient); + + // Todo + }); + }); + + describe('evaluate', () => { + const validRequest: AccessEvaluationRequest = { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }; + + it('should make correct API call for access evaluation', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ decision: true }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + const response = await client.evaluate(validRequest); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/access/v1/evaluation', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(validRequest), + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer test-token', + 'X-Request-ID': expect.stringMatching(/^authzen-\d+-[a-z0-9]+$/), + }), + signal: expect.any(AbortSignal), + }) + ); + + expect(response).toEqual({ decision: true }); + }); + + it('should handle HTTP errors', async () => { + const mockResponse = { + ok: false, + status: 403, + statusText: 'Forbidden', + json: jest.fn().mockResolvedValue({ message: 'Access denied' }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + await expect(client.evaluate(validRequest)).rejects.toThrow(AuthZenRequestError); + }); + + it('should handle network errors', async () => { + mockFetch.mockRejectedValue(new Error('Network error')); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + await expect(client.evaluate(validRequest)).rejects.toThrow(AuthZenNetworkError); + }); + + it('should handle timeout errors', async () => { + // Create a promise that will be rejected with AbortError when signal is aborted + mockFetch.mockImplementation((url, options) => { + return new Promise((resolve, reject) => { + // Set up abort signal listener + if (options?.signal) { + const abortHandler = () => { + const error = new Error('The operation was aborted'); + error.name = 'AbortError'; + reject(error); + }; + + options.signal.addEventListener('abort', abortHandler); + + // Clean up listener if request completes normally + options.signal.addEventListener('abort', () => { + options.signal.removeEventListener('abort', abortHandler); + }); + } + }); + }); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + timeout: 1000, + }); + + const evaluatePromise = client.evaluate(validRequest); + + // Advance time to trigger timeout + jest.advanceTimersByTime(1100); // Slightly more than timeout + + await expect(evaluatePromise).rejects.toThrow(AuthZenNetworkError); + await expect(evaluatePromise).rejects.toThrow('Request timeout after 1000ms'); + }); + + it('should handle invalid JSON responses', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockRejectedValue(new Error('Invalid JSON')), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + await expect(client.evaluate(validRequest)).rejects.toThrow(AuthZenResponseError); + }); + + it('should handle non-JSON responses', async () => { + const mockResponse = { + ok: true, + status: 200, + text: jest.fn().mockResolvedValue('Not JSON'), + headers: { + get: jest.fn().mockReturnValue('text/plain') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + await expect(client.evaluate(validRequest)).rejects.toThrow(AuthZenResponseError); + }); + + it('should validate request before sending', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const invalidRequest = { + subject: { type: 'user' }, // Missing id + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + } as AccessEvaluationRequest; + + await expect(client.evaluate(invalidRequest)).rejects.toThrow(AuthZenValidationError); + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + + describe('evaluations', () => { + const validRequest: AccessEvaluationsRequest = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }, + { + subject: { type: 'user', id: 'bob@example.com' }, + action: { name: 'can_write' }, + resource: { type: 'document', id: '456' }, + }, + ], + }; + + it('should make correct API call for batch evaluation', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }, { decision: false }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const response = await client.evaluations(validRequest); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/access/v1/evaluations', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(validRequest), + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Request-ID': expect.stringMatching(/^authzen-\d+-[a-z0-9]+$/), + }), + signal: expect.any(AbortSignal), + }) + ); + + expect(response).toEqual({ + evaluations: [{ decision: true }, { decision: false }], + }); + }); + + it('should validate evaluations_semantic in options', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const invalidRequest = { + evaluations: validRequest.evaluations, + options: { + evaluations_semantic: 'invalid_semantic' as any, + }, + }; + + await expect(client.evaluations(invalidRequest)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(invalidRequest)).rejects.toThrow('Invalid evaluations_semantic'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should accept valid evaluations_semantic values', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }, { decision: false }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const validSemantics = ['execute_all', 'deny_on_first_deny', 'permit_on_first_permit']; + + for (const semantic of validSemantics) { + mockFetch.mockClear(); + + const requestWithOptions = { + evaluations: validRequest.evaluations, + options: { + evaluations_semantic: semantic as any, + }, + }; + + await expect(client.evaluations(requestWithOptions)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + } + }); + + it('should allow request without options', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }, { decision: false }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithoutOptions = { + evaluations: validRequest.evaluations, + // No options property + }; + + await expect(client.evaluations(requestWithoutOptions)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should allow empty options object', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }, { decision: false }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithEmptyOptions = { + evaluations: validRequest.evaluations, + options: {}, + }; + + await expect(client.evaluations(requestWithEmptyOptions)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should validate that options is an object if provided', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const invalidRequest = { + evaluations: validRequest.evaluations, + options: 'not an object' as any, + }; + + await expect(client.evaluations(invalidRequest)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(invalidRequest)).rejects.toThrow('Options must be an object'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should validate empty evaluations array', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const invalidRequest = { + evaluations: [], + }; + + await expect(client.evaluations(invalidRequest)).rejects.toThrow(AuthZenValidationError); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should validate individual evaluations in batch', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const invalidRequest = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }, + { + subject: { type: 'user' }, // Missing id + action: { name: 'can_read' }, + resource: { type: 'document', id: '456' }, + }, + ], + } as AccessEvaluationsRequest; + + await expect(client.evaluations(invalidRequest)).rejects.toThrow(AuthZenValidationError); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should validate default values structure', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithDefaults = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }, + ], + subject: { type: 'user' }, + resource: { type: 'document' }, + action: { name: 'can_read' }, + context: { environment: 'test' }, + }; + + await expect(client.evaluations(requestWithDefaults)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + }); + + describe('default value handling', () => { + it('should allow missing subject in evaluations when default subject is provided', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }, { decision: false }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithDefaultSubject = { + evaluations: [ + { + // No subject - should use default + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }, + { + // No subject - should use default + action: { name: 'can_write' }, + resource: { type: 'document', id: '456' }, + }, + ], + subject: { type: 'user', id: 'alice@example.com' }, // Default subject + }; + + await expect(client.evaluations(requestWithDefaultSubject)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should allow missing resource in evaluations when default resource is provided', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }, { decision: false }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithDefaultResource = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + // No resource - should use default + }, + { + subject: { type: 'user', id: 'bob@example.com' }, + action: { name: 'can_write' }, + // No resource - should use default + }, + ], + resource: { type: 'document', id: 'shared-doc' }, // Default resource + }; + + await expect(client.evaluations(requestWithDefaultResource)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should allow missing action in evaluations when default action is provided', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }, { decision: false }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithDefaultAction = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + resource: { type: 'document', id: '123' }, + // No action - should use default + }, + { + subject: { type: 'user', id: 'bob@example.com' }, + resource: { type: 'document', id: '456' }, + // No action - should use default + }, + ], + action: { name: 'can_read' }, // Default action + }; + + await expect(client.evaluations(requestWithDefaultAction)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should allow completely empty evaluations when all defaults are provided', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithAllDefaults = { + evaluations: [ + { + // Completely empty evaluation - should use all defaults + }, + ], + subject: { type: 'user', id: 'alice@example.com' }, + resource: { type: 'document', id: 'shared-doc' }, + action: { name: 'can_read' }, + context: { environment: 'test' } + }; + + await expect(client.evaluations(requestWithAllDefaults)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should still require subject when no default subject is provided', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestMissingSubject = { + evaluations: [ + { + // No subject and no default subject + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }, + ], + // No default subject provided + resource: { type: 'document', id: 'shared-doc' }, + action: { name: 'can_read' }, + }; + + await expect(client.evaluations(requestMissingSubject)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestMissingSubject)).rejects.toThrow('Subject is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should still require resource when no default resource is provided', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestMissingResource = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + // No resource and no default resource + }, + ], + subject: { type: 'user', id: 'bob@example.com' }, + action: { name: 'can_write' }, + // No default resource provided + }; + + await expect(client.evaluations(requestMissingResource)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestMissingResource)).rejects.toThrow('Resource is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should still require action when no default action is provided', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestMissingAction = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + resource: { type: 'document', id: '123' }, + // No action and no default action + }, + ], + subject: { type: 'user', id: 'bob@example.com' }, + resource: { type: 'document', id: 'shared-doc' }, + // No default action provided + }; + + await expect(client.evaluations(requestMissingAction)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestMissingAction)).rejects.toThrow('Action is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + + describe('partial element validation', () => { + it('should reject partial subject (missing type) in evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialSubject = { + evaluations: [ + { + subject: { id: 'alice@example.com' }, // Missing type - partial subject not allowed + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }, + ], + } as AccessEvaluationsRequest; + + await expect(client.evaluations(requestWithPartialSubject)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialSubject)).rejects.toThrow('Subject type is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should reject partial subject (missing id) in evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialSubject = { + evaluations: [ + { + subject: { type: 'user' }, // Missing id - partial subject not allowed + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }, + ], + } as AccessEvaluationsRequest; + + await expect(client.evaluations(requestWithPartialSubject)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialSubject)).rejects.toThrow('Subject id is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should reject partial resource (missing type) in evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialResource = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { id: '123' }, // Missing type - partial resource not allowed + }, + ], + } as AccessEvaluationsRequest; + + await expect(client.evaluations(requestWithPartialResource)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialResource)).rejects.toThrow('Resource type is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should reject partial resource (missing id) in evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialResource = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document' }, // Missing id - partial resource not allowed + }, + ], + } as AccessEvaluationsRequest; + + await expect(client.evaluations(requestWithPartialResource)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialResource)).rejects.toThrow('Resource id is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should reject partial action (missing name) in evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialAction = { + evaluations: [ + { + subject: { type: 'user', id: 'alice@example.com' }, + action: { properties: { method: 'GET' } }, // Missing name - partial action not allowed + resource: { type: 'document', id: '123' }, + }, + ], + } as unknown as AccessEvaluationsRequest; + + await expect(client.evaluations(requestWithPartialAction)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialAction)).rejects.toThrow('Action name is required'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should reject partial subject in defaults when used without evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialDefaultSubject = { + // No evaluations array + subject: { type: 'user' }, // Missing id - partial default not allowed + resource: { type: 'document', id: 'doc123' }, + action: { name: 'read' } + }; + + await expect(client.evaluations(requestWithPartialDefaultSubject)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialDefaultSubject)).rejects.toThrow( + 'When no evaluations array is provided, default subject with type and id is required' + ); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should reject partial resource in defaults when used without evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialDefaultResource = { + // No evaluations array + subject: { type: 'user', id: 'alice@example.com' }, + resource: { type: 'document' }, // Missing id - partial default not allowed + action: { name: 'read' } + }; + + await expect(client.evaluations(requestWithPartialDefaultResource)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialDefaultResource)).rejects.toThrow( + 'When no evaluations array is provided, default resource with type and id is required' + ); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should reject partial action in defaults when used without evaluations', async () => { + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithPartialDefaultAction = { + // No evaluations array + subject: { type: 'user', id: 'alice@example.com' }, + resource: { type: 'document', id: 'doc123' }, + action: { properties: { method: 'GET' } } // Missing name - partial default not allowed + }; + + await expect(client.evaluations(requestWithPartialDefaultAction)).rejects.toThrow(AuthZenValidationError); + await expect(client.evaluations(requestWithPartialDefaultAction)).rejects.toThrow( + 'When no evaluations array is provided, default action with name is required' + ); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('should allow complete elements with properties', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + evaluations: [{ decision: true }], + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const requestWithCompleteElements = { + evaluations: [ + { + subject: { + type: 'user', + id: 'alice@example.com', + properties: { role: 'admin', department: 'engineering' } + }, + action: { + name: 'can_read', + properties: { method: 'GET', scope: 'full' } + }, + resource: { + type: 'document', + id: '123', + properties: { classification: 'confidential', owner: 'alice' } + }, + }, + ], + }; + + await expect(client.evaluations(requestWithCompleteElements)).resolves.toBeDefined(); + expect(mockFetch).toHaveBeenCalled(); + }); + }); + + describe('request validation', () => { + let client: AuthZenClient; + + beforeEach(() => { + client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + }); + + it('should validate subject type is required', async () => { + const invalidRequest = { + subject: { id: 'alice@example.com' }, // Missing type + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + } as AccessEvaluationRequest; + + await expect(client.evaluate(invalidRequest)).rejects.toThrow(AuthZenValidationError); + }); + + it('should validate subject id is required', async () => { + const invalidRequest = { + subject: { type: 'user' }, // Missing id + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + } as AccessEvaluationRequest; + + await expect(client.evaluate(invalidRequest)).rejects.toThrow(AuthZenValidationError); + }); + + it('should validate resource type is required', async () => { + const invalidRequest = { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { id: '123' }, // Missing type + } as AccessEvaluationRequest; + + await expect(client.evaluate(invalidRequest)).rejects.toThrow(AuthZenValidationError); + }); + + it('should validate resource id is required', async () => { + const invalidRequest = { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document' }, // Missing id + } as AccessEvaluationRequest; + + await expect(client.evaluate(invalidRequest)).rejects.toThrow(AuthZenValidationError); + }); + + it('should validate action name is required', async () => { + const invalidRequest = { + subject: { type: 'user', id: 'alice@example.com' }, + action: {}, // Missing name + resource: { type: 'document', id: '123' }, + } as AccessEvaluationRequest; + + await expect(client.evaluate(invalidRequest)).rejects.toThrow(AuthZenValidationError); + }); + }); + + describe('error handling', () => { + let client: AuthZenClient; + + beforeEach(() => { + client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + }); + + it('should wrap unknown errors as AuthZenError', async () => { + mockFetch.mockRejectedValue(new Error('Unknown error')); + + const validRequest: AccessEvaluationRequest = { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }; + + await expect(client.evaluate(validRequest)).rejects.toThrow(AuthZenError); + }); + + it('should preserve AuthZen errors', async () => { + const customError = new AuthZenValidationError('Custom validation error'); + + // Mock the validation to throw our custom error + jest.spyOn(client as any, 'validateEvaluationRequest').mockImplementation(() => { + throw customError; + }); + + const validRequest: AccessEvaluationRequest = { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }; + + await expect(client.evaluate(validRequest)).rejects.toBe(customError); + }); + }); + + describe('request ID generation', () => { + it('should generate unique request IDs', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ decision: true }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + }); + + const validRequest: AccessEvaluationRequest = { + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, + }; + + await client.evaluate(validRequest); + await client.evaluate(validRequest); + + const calls = mockFetch.mock.calls; + const requestId1 = calls[0][1].headers['X-Request-ID']; + const requestId2 = calls[1][1].headers['X-Request-ID']; + + expect(requestId1).toMatch(/^authzen-\d+-[a-z0-9]+$/); + expect(requestId2).toMatch(/^authzen-\d+-[a-z0-9]+$/); + expect(requestId1).not.toBe(requestId2); + }); + }); +}); diff --git a/src/Typescript/src/client.ts b/src/Typescript/src/client.ts new file mode 100644 index 0000000..a83cdc2 --- /dev/null +++ b/src/Typescript/src/client.ts @@ -0,0 +1,500 @@ +import { + AuthZenClientConfig, + AccessEvaluationRequest, + AccessEvaluationResponse, + AccessEvaluationsRequest, + AccessEvaluationsResponse, + AuthZenConfiguration, + AuthZenError, + AuthZenRequestError, + AuthZenResponseError, + AuthZenNetworkError, + AuthZenValidationError, + AuthZenDiscoveryError, + Subject, + Resource, + Action, + Context, +} from './types'; + +interface IAuthZenClient { + discover(): Promise; + evaluate(request: AccessEvaluationRequest): Promise; + evaluations(request: AccessEvaluationsRequest): Promise; +} + +/** + * AuthZen Client for making authorization requests to a Policy Decision Point (PDP) + */ +export class AuthZenClient implements IAuthZenClient { + readonly pdpUrl: string; + private readonly headers: Record; + private readonly timeout: number; + + constructor(config: AuthZenClientConfig) { + this.pdpUrl = config.pdpUrl.replace(/\/$/, ''); // Remove trailing slash + this.timeout = config.timeout || 10000; // Default 10 seconds + + this.headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...config.headers, + }; + + // Add Authorization header if token provided + if (config.token) { + this.headers['Authorization'] = `Bearer ${config.token}`; + } + } + + /** + * Discover AuthZen configuration from the well-known endpoint + * + * @returns Promise The discovered configuration + * @throws {AuthZenDiscoveryError} When discovery fails or configuration is invalid + * @throws {AuthZenNetworkError} When network request fails + * @throws {AuthZenRequestError} When HTTP request returns an error status + */ + async discover(): Promise { + try { + const response = await this.makeRequest('/.well-known/authzen-configuration', { + method: 'GET', + }); + + const config = response as AuthZenConfiguration; + this.validateDiscoveryConfiguration(config); + + return config; + } catch (error) { + if (error instanceof AuthZenError) { + throw error; + } + throw this.handleError(error, 'discover'); + } + } + + /** + * Make a single access evaluation request + */ + async evaluate(request: AccessEvaluationRequest): Promise { + this.validateEvaluationRequest(request); + + try { + const response = await this.makeRequest('/access/v1/evaluation', { + method: 'POST', + body: JSON.stringify(request), + }); + + return response as AccessEvaluationResponse; + } catch (error) { + throw this.handleError(error, 'evaluate'); + } + } + + /** + * Make multiple access evaluation requests in a single call + */ + async evaluations(request: AccessEvaluationsRequest): Promise { + this.validateEvaluationsRequest(request); + + try { + const response = await this.makeRequest('/access/v1/evaluations', { + method: 'POST', + body: JSON.stringify(request), + }); + + return response as AccessEvaluationsResponse; + } catch (error) { + throw this.handleError(error, 'evaluations'); + } + } + + /** + * Make an HTTP request to the PDP + */ + private async makeRequest(endpoint: string, options: RequestInit): Promise { + const url = `${this.pdpUrl}${endpoint}`; + const requestId = this.generateRequestId(); + + // Create abort controller for timeout handling + const abortController = new AbortController(); + const timeoutId = setTimeout(() => { + abortController.abort(); + }, this.timeout); + + const requestOptions: RequestInit = { + ...options, + headers: { + ...this.headers, + 'X-Request-ID': requestId, + ...options.headers, + }, + signal: abortController.signal, + }; + + let response: Response; + + try { + response = await fetch(url, requestOptions); + } catch (error: any) { + clearTimeout(timeoutId); + + if (error.name === 'AbortError') { + throw new AuthZenNetworkError(`Request timeout after ${this.timeout}ms`, requestId); + } + + throw new AuthZenNetworkError(`Network error: ${error.message}`, requestId); + } finally { + clearTimeout(timeoutId); + } + + // Handle non-JSON responses + let responseData: any; + const contentType = response.headers.get('content-type'); + + if (contentType && contentType.includes('application/json')) { + try { + responseData = await response.json(); + } catch (error) { + throw new AuthZenResponseError( + 'Invalid JSON in response', + response.status, + requestId + ); + } + } else { + const text = await response.text(); + throw new AuthZenResponseError( + `Expected JSON response, got: ${contentType}. Body: ${text}`, + response.status, + requestId + ); + } + + if (!response.ok) { + throw new AuthZenRequestError( + responseData?.message || `HTTP ${response.status}: ${response.statusText}`, + response.status, + requestId, + responseData + ); + } + + return responseData; + } + + /** + * Validate discovery configuration response + */ + private validateDiscoveryConfiguration(config: AuthZenConfiguration): void { + if (!config || typeof config !== 'object') { + throw new AuthZenDiscoveryError('Discovery configuration must be an object'); + } + + if (!config.policy_decision_point || typeof config.policy_decision_point !== 'string') { + throw new AuthZenDiscoveryError('Discovery configuration must have a policy_decision_point string property'); + } + + // Validate that policy_decision_point is a valid URL + try { + new URL(config.policy_decision_point); + } catch { + throw new AuthZenDiscoveryError('policy_decision_point must be a valid URL'); + } + + // Validate optional endpoint URLs if present + const endpointProperties = [ + 'access_evaluation_endpoint', + 'access_evaluations_endpoint', + 'search_subject_endpoint', + 'search_resource_endpoint', + 'search_action_endpoint', + ]; + + for (const prop of endpointProperties) { + const value = config[prop]; + if (value !== undefined) { + if (typeof value !== 'string') { + throw new AuthZenDiscoveryError(`${prop} must be a string if provided`); + } + if (value.length === 0) { + throw new AuthZenDiscoveryError(`${prop} cannot be an empty string`); + } + // Validate that it's either a full URL or a relative path + if (!value.startsWith('/') && !value.startsWith('http')) { + throw new AuthZenDiscoveryError(`${prop} must be either a relative path starting with '/' or a full URL`); + } + } + } + } + + /** + * Validate an evaluation request, considering default values + */ + private validateEvaluationRequestWithDefaults( + evaluation: AccessEvaluationRequest, + defaults?: { + subject?: Partial; + resource?: Partial; + action?: Partial; + context?: Context; + } + ): void { + if (!evaluation || typeof evaluation !== 'object') { + throw new AuthZenValidationError('Evaluation must be an object'); + } + + // Check if subject is provided or can use default + const hasSubject = evaluation.subject && Object.keys(evaluation.subject).length > 0; + const hasDefaultSubject = defaults?.subject && Object.keys(defaults.subject).length > 0; + + if (hasSubject) { + // If subject is provided, it must be complete + if (!evaluation.subject || typeof evaluation.subject !== 'object') { + throw new AuthZenValidationError('Subject is required and must be an object'); + } + if (!evaluation.subject.type || typeof evaluation.subject.type !== 'string') { + throw new AuthZenValidationError('Subject type is required and must be a string'); + } + if (!evaluation.subject.id || typeof evaluation.subject.id !== 'string') { + throw new AuthZenValidationError('Subject id is required and must be a string'); + } + } else if (!hasDefaultSubject) { + // No subject provided and no default available + throw new AuthZenValidationError('Subject is required'); + } + + // Check if resource is provided or can use default + const hasResource = evaluation.resource && Object.keys(evaluation.resource).length > 0; + const hasDefaultResource = defaults?.resource && Object.keys(defaults.resource).length > 0; + + if (hasResource) { + // If resource is provided, it must be complete + if (!evaluation.resource || typeof evaluation.resource !== 'object') { + throw new AuthZenValidationError('Resource is required and must be an object'); + } + if (!evaluation.resource.type || typeof evaluation.resource.type !== 'string') { + throw new AuthZenValidationError('Resource type is required and must be a string'); + } + if (!evaluation.resource.id || typeof evaluation.resource.id !== 'string') { + throw new AuthZenValidationError('Resource id is required and must be a string'); + } + } else if (!hasDefaultResource) { + // No resource provided and no default available + throw new AuthZenValidationError('Resource is required'); + } + + // Check if action is provided or can use default + const hasAction = evaluation.action && Object.keys(evaluation.action).length > 0; + const hasDefaultAction = defaults?.action && Object.keys(defaults.action).length > 0; + + if (hasAction) { + // If action is provided, it must be complete + if (!evaluation.action || typeof evaluation.action !== 'object' || !evaluation.action.name || typeof evaluation.action.name !== 'string') { + throw new AuthZenValidationError('Action name is required and must be a string'); + } + } else if (!hasDefaultAction) { + // No action provided and no default available + throw new AuthZenValidationError('Action is required'); + } + + // Validate context if present + if (evaluation.context) { + this.validateContext(evaluation.context); + } + } + + /** + * Validate context - can be any JSON object + */ + private validateContext(context: Context): void { + if (context === null || context === undefined) { + throw new AuthZenValidationError('Context cannot be null or undefined'); + } + + if (typeof context !== 'object') { + throw new AuthZenValidationError('Context must be an object'); + } + + if (Array.isArray(context)) { + throw new AuthZenValidationError('Context cannot be an array'); + } + + // Validate that context contains only JSON-serializable values + try { + JSON.stringify(context); + } catch (error) { + throw new AuthZenValidationError('Context must contain only JSON-serializable values'); + } + } + + /** + * Validate a single evaluation request (original method for single evaluations) + */ + private validateEvaluationRequest(request: AccessEvaluationRequest): void { + if (!request || typeof request !== 'object') { + throw new AuthZenValidationError('Request must be an object'); + } + + // For single evaluations, all fields are required + if (!request.subject || typeof request.subject !== 'object') { + throw new AuthZenValidationError('Subject is required'); + } + + if (!request.subject.type || typeof request.subject.type !== 'string') { + throw new AuthZenValidationError('Subject type is required and must be a string'); + } + + if (!request.subject.id || typeof request.subject.id !== 'string') { + throw new AuthZenValidationError('Subject id is required and must be a string'); + } + + if (!request.resource || typeof request.resource !== 'object') { + throw new AuthZenValidationError('Resource is required'); + } + + if (!request.resource.type || typeof request.resource.type !== 'string') { + throw new AuthZenValidationError('Resource type is required and must be a string'); + } + + if (!request.resource.id || typeof request.resource.id !== 'string') { + throw new AuthZenValidationError('Resource id is required and must be a string'); + } + + if (!request.action || typeof request.action !== 'object') { + throw new AuthZenValidationError('Action is required'); + } + + if (!request.action.name || typeof request.action.name !== 'string') { + throw new AuthZenValidationError('Action name is required and must be a string'); + } + + if (request.context) { + this.validateContext(request.context); + } + } + + /** + * Validate a batch evaluations request + */ + private validateEvaluationsRequest(request: AccessEvaluationsRequest): void { + if (!request || typeof request !== 'object') { + throw new AuthZenValidationError('Request must be an object'); + } + + // Extract defaults for validation + const defaults = { + subject: request.subject, + resource: request.resource, + action: request.action, + context: request.context, + }; + + // If evaluations array is provided, validate it + if (request.evaluations !== undefined) { + if (!Array.isArray(request.evaluations)) { + throw new AuthZenValidationError('Evaluations must be an array if provided'); + } + + if (request.evaluations.length === 0) { + throw new AuthZenValidationError('Evaluations array cannot be empty if provided'); + } + + // Validate each evaluation with defaults + request.evaluations.forEach((evaluation, index) => { + try { + this.validateEvaluationRequestWithDefaults(evaluation, defaults); + } catch (error) { + if (error instanceof AuthZenValidationError) { + throw new AuthZenValidationError( + `Evaluation at index ${index}: ${error.message}` + ); + } + throw error; + } + }); + } else { + // If no evaluations array, validate that required defaults are present and complete + if (!request.subject || !request.subject.type || !request.subject.id) { + throw new AuthZenValidationError( + 'When no evaluations array is provided, default subject with type and id is required' + ); + } + + if (!request.resource || !request.resource.type || !request.resource.id) { + throw new AuthZenValidationError( + 'When no evaluations array is provided, default resource with type and id is required' + ); + } + + if (!request.action || !request.action.name) { + throw new AuthZenValidationError( + 'When no evaluations array is provided, default action with name is required' + ); + } + } + + // Validate options if provided + if (request.options) { + if (typeof request.options !== 'object') { + throw new AuthZenValidationError('Options must be an object'); + } + + if (request.options.evaluations_semantic) { + const validSemantics = ['execute_all', 'deny_on_first_deny', 'permit_on_first_permit']; + if (!validSemantics.includes(request.options.evaluations_semantic)) { + throw new AuthZenValidationError( + `Invalid evaluations_semantic. Must be one of: ${validSemantics.join(', ')}` + ); + } + } + } + + // Validate default values structure if provided + if (request.subject) { + if (request.subject.type !== undefined && typeof request.subject.type !== 'string') { + throw new AuthZenValidationError('Default subject type must be a string'); + } + if (request.subject.id !== undefined && typeof request.subject.id !== 'string') { + throw new AuthZenValidationError('Default subject id must be a string'); + } + } + + if (request.resource) { + if (request.resource.type !== undefined && typeof request.resource.type !== 'string') { + throw new AuthZenValidationError('Default resource type must be a string'); + } + if (request.resource.id !== undefined && typeof request.resource.id !== 'string') { + throw new AuthZenValidationError('Default resource id must be a string'); + } + } + + if (request.action) { + if (request.action.name !== undefined && typeof request.action.name !== 'string') { + throw new AuthZenValidationError('Default action name must be a string'); + } + } + + if (request.context) { + this.validateContext(request.context); + } + } + + /** + * Handle and wrap errors appropriately + */ + private handleError(error: any, operation: string): AuthZenError { + if (error instanceof AuthZenError) { + return error; + } + + return new AuthZenError(`Error during ${operation}: ${error.message}`); + } + + /** + * Generate a unique request ID for correlation + */ + private generateRequestId(): string { + return `authzen-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + } +} diff --git a/src/Typescript/src/discovery.test.ts b/src/Typescript/src/discovery.test.ts new file mode 100644 index 0000000..ea383a9 --- /dev/null +++ b/src/Typescript/src/discovery.test.ts @@ -0,0 +1,565 @@ +import { AuthZenClient } from './client'; +import { + AuthZenConfiguration, + AuthZenDiscoveryError, + AuthZenRequestError, + AuthZenResponseError, + AuthZenNetworkError, +} from './types'; + +// Mock global fetch +const mockFetch = jest.fn(); +global.fetch = mockFetch; + +describe('AuthZenClient - Discovery', () => { + beforeEach(() => { + mockFetch.mockClear(); + jest.clearAllTimers(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('discover()', () => { + let client: AuthZenClient; + + beforeEach(() => { + client = new AuthZenClient({ + pdpUrl: 'https://pdp.example.com', + token: 'test-token', + }); + }); + + describe('successful discovery', () => { + it('should discover minimal valid configuration', async () => { + const mockConfig: AuthZenConfiguration = { + policy_decision_point: 'https://pdp.example.com', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const result = await client.discover(); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://pdp.example.com/.well-known/authzen-configuration', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer test-token', + 'X-Request-ID': expect.stringMatching(/^authzen-\d+-[a-z0-9]+$/), + }), + signal: expect.any(AbortSignal), + }) + ); + + expect(result).toEqual(mockConfig); + }); + + it('should discover full configuration with all optional endpoints', async () => { + const mockConfig: AuthZenConfiguration = { + policy_decision_point: 'https://pdp.example.com', + access_evaluation_endpoint: 'https://pdp.example.com/access/v1/evaluation', + access_evaluations_endpoint: 'https://pdp.example.com/access/v1/evaluations', + search_subject_endpoint: 'https://pdp.example.com/access/v1/search/subject', + search_resource_endpoint: 'https://pdp.example.com/access/v1/search/resource', + search_action_endpoint: 'https://pdp.example.com/access/v1/search/action', + custom_endpoint: 'https://pdp.example.com/custom', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const result = await client.discover(); + + expect(result).toEqual(mockConfig); + }); + + it('should discover configuration with relative paths', async () => { + const mockConfig: AuthZenConfiguration = { + policy_decision_point: 'https://pdp.example.com', + access_evaluation_endpoint: '/access/v1/evaluation', + access_evaluations_endpoint: '/access/v1/evaluations', + search_subject_endpoint: '/access/v1/search/subject', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const result = await client.discover(); + + expect(result).toEqual(mockConfig); + }); + + it('should work without authentication token', async () => { + const clientWithoutToken = new AuthZenClient({ + pdpUrl: 'https://pdp.example.com', + }); + + const mockConfig: AuthZenConfiguration = { + policy_decision_point: 'https://pdp.example.com', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + const result = await clientWithoutToken.discover(); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://pdp.example.com/.well-known/authzen-configuration', + expect.objectContaining({ + headers: expect.not.objectContaining({ + 'Authorization': expect.anything(), + }), + }) + ); + + expect(result).toEqual(mockConfig); + }); + }); + + describe('validation errors', () => { + it('should throw AuthZenDiscoveryError for non-object response', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue('not an object'), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow('Discovery configuration must be an object'); + }); + + it('should throw AuthZenDiscoveryError for missing policy_decision_point', async () => { + const mockConfig = { + access_evaluation_endpoint: '/access/v1/evaluation', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow('Discovery configuration must have a policy_decision_point string property'); + }); + + it('should throw AuthZenDiscoveryError for non-string policy_decision_point', async () => { + const mockConfig = { + policy_decision_point: 123, + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow('Discovery configuration must have a policy_decision_point string property'); + }); + + it('should throw AuthZenDiscoveryError for invalid policy_decision_point URL', async () => { + const mockConfig = { + policy_decision_point: 'not-a-valid-url', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow('policy_decision_point must be a valid URL'); + }); + + it('should throw AuthZenDiscoveryError for non-string optional endpoints', async () => { + const mockConfig = { + policy_decision_point: 'https://pdp.example.com', + access_evaluation_endpoint: 123, + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow('access_evaluation_endpoint must be a string if provided'); + }); + + it('should throw AuthZenDiscoveryError for empty string endpoints', async () => { + const mockConfig = { + policy_decision_point: 'https://pdp.example.com', + access_evaluations_endpoint: '', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow('access_evaluations_endpoint cannot be an empty string'); + }); + + it('should throw AuthZenDiscoveryError for invalid endpoint paths', async () => { + const mockConfig = { + policy_decision_point: 'https://pdp.example.com', + search_subject_endpoint: 'invalid-path', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow('search_subject_endpoint must be either a relative path starting with \'/\' or a full URL'); + }); + + it('should validate all optional endpoint properties', async () => { + const endpointProperties = [ + 'access_evaluation_endpoint', + 'access_evaluations_endpoint', + 'search_subject_endpoint', + 'search_resource_endpoint', + 'search_action_endpoint', + ]; + + for (const prop of endpointProperties) { + mockFetch.mockClear(); + + const mockConfig = { + policy_decision_point: 'https://pdp.example.com', + [prop]: 123, // Invalid type + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenDiscoveryError); + await expect(client.discover()).rejects.toThrow(`${prop} must be a string if provided`); + } + }); + }); + + describe('HTTP errors', () => { + it('should handle 404 Not Found', async () => { + const mockResponse = { + ok: false, + status: 404, + statusText: 'Not Found', + json: jest.fn().mockResolvedValue({ message: 'Discovery endpoint not found' }), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenRequestError); + await expect(client.discover()).rejects.toThrow('Discovery endpoint not found'); + }); + + it('should handle 500 Internal Server Error', async () => { + const mockResponse = { + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: jest.fn().mockResolvedValue({}), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenRequestError); + await expect(client.discover()).rejects.toThrow('HTTP 500: Internal Server Error'); + }); + + it('should handle 403 Forbidden', async () => { + const mockResponse = { + ok: false, + status: 403, + statusText: 'Forbidden', + json: jest.fn().mockResolvedValue({ message: 'Access denied' }), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenRequestError); + + try { + await client.discover(); + expect(true).toBe(false); // Should not reach here + } catch (error: unknown) { + expect(error).toBeInstanceOf(AuthZenRequestError); + if (error instanceof AuthZenRequestError) { + expect(error.statusCode).toBe(403); + expect(error.responseData).toEqual({ message: 'Access denied' }); + } + } + }); + }); + + describe('network errors', () => { + it('should handle network timeouts', async () => { + // Mock fetch to simulate hanging request + mockFetch.mockImplementation((url, options) => { + return new Promise((resolve, reject) => { + // Set up abort signal listener + if (options?.signal) { + const abortHandler = () => { + const error = new Error('The operation was aborted'); + error.name = 'AbortError'; + reject(error); + }; + + options.signal.addEventListener('abort', abortHandler); + } + }); + }); + + const promise = client.discover(); + jest.advanceTimersByTime(10000); + + await expect(promise).rejects.toThrow(AuthZenNetworkError); + await expect(promise).rejects.toThrow('Request timeout after 10000ms'); + }); + + it('should handle connection errors', async () => { + mockFetch.mockRejectedValue(new Error('Connection refused')); + + await expect(client.discover()).rejects.toThrow(AuthZenNetworkError); + await expect(client.discover()).rejects.toThrow('Network error: Connection refused'); + }); + + it('should handle DNS resolution errors', async () => { + mockFetch.mockRejectedValue(new Error('getaddrinfo ENOTFOUND')); + + await expect(client.discover()).rejects.toThrow(AuthZenNetworkError); + await expect(client.discover()).rejects.toThrow('Network error: getaddrinfo ENOTFOUND'); + }); + }); + + describe('response parsing errors', () => { + it('should handle invalid JSON responses', async () => { + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockRejectedValue(new Error('Invalid JSON')), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenResponseError); + await expect(client.discover()).rejects.toThrow('Invalid JSON in response'); + }); + + it('should handle non-JSON content type', async () => { + const mockResponse = { + ok: true, + status: 200, + text: jest.fn().mockResolvedValue('Not JSON'), + headers: { + get: jest.fn().mockReturnValue('text/html'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenResponseError); + await expect(client.discover()).rejects.toThrow('Expected JSON response, got: text/html'); + }); + + it('should handle missing content type', async () => { + const mockResponse = { + ok: true, + status: 200, + text: jest.fn().mockResolvedValue('some text'), + headers: { + get: jest.fn().mockReturnValue(null), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await expect(client.discover()).rejects.toThrow(AuthZenResponseError); + await expect(client.discover()).rejects.toThrow('Expected JSON response, got: null'); + }); + }); + + describe('request correlation', () => { + it('should include request ID in headers', async () => { + const mockConfig: AuthZenConfiguration = { + policy_decision_point: 'https://pdp.example.com', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await client.discover(); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Request-ID': expect.stringMatching(/^authzen-\d+-[a-z0-9]+$/), + }), + }) + ); + }); + + it('should generate unique request IDs for multiple calls', async () => { + const mockConfig: AuthZenConfiguration = { + policy_decision_point: 'https://pdp.example.com', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + await client.discover(); + await client.discover(); + + const calls = mockFetch.mock.calls; + const requestId1 = calls[0][1].headers['X-Request-ID']; + const requestId2 = calls[1][1].headers['X-Request-ID']; + + expect(requestId1).toMatch(/^authzen-\d+-[a-z0-9]+$/); + expect(requestId2).toMatch(/^authzen-\d+-[a-z0-9]+$/); + expect(requestId1).not.toBe(requestId2); + }); + }); + + describe('error context', () => { + it('should not include request ID in discovery validation errors', async () => { + const mockConfig = { + policy_decision_point: 'invalid-url', + }; + + const mockResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue(mockConfig), + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, + }; + mockFetch.mockResolvedValue(mockResponse); + + try { + await client.discover(); + expect(true).toBe(false); // Should not reach here + } catch (error: unknown) { + expect(error).toBeInstanceOf(AuthZenDiscoveryError); + if (error instanceof AuthZenDiscoveryError) { + expect(error.requestId).toBeUndefined(); // Discovery validation errors don't include request ID + expect(error.message).toBe('policy_decision_point must be a valid URL'); + } + } + }); + + it('should include request ID in network errors', async () => { + mockFetch.mockRejectedValue(new Error('Network error')); + + try { + await client.discover(); + expect(true).toBe(false); // Should not reach here + } catch (error: unknown) { + expect(error).toBeInstanceOf(AuthZenNetworkError); + if (error instanceof AuthZenNetworkError) { + expect(error.requestId).toMatch(/^authzen-\d+-[a-z0-9]+$/); + } + } + }); + }); + }); +}); \ No newline at end of file diff --git a/src/Typescript/src/index.ts b/src/Typescript/src/index.ts new file mode 100644 index 0000000..05281a1 --- /dev/null +++ b/src/Typescript/src/index.ts @@ -0,0 +1,26 @@ +/** + * AuthZen TypeScript Client Library + * + * A TypeScript client library for interacting with AuthZen-compliant + * Policy Decision Points (PDPs) according to the AuthZen Authorization API 1.0 specification. + */ + +export { AuthZenClient } from './client'; +export * from './types'; + +// Re-export commonly used types for convenience +export type { + AuthZenClientConfig, + AccessEvaluationRequest, + AccessEvaluationResponse, + AccessEvaluationsRequest, + AccessEvaluationsResponse, + BatchEvaluationOptions, + AuthZenConfiguration, + Subject, + Resource, + Action, + Context, + EvaluationSemantics, + EvaluationOptions, +} from './types'; diff --git a/src/Typescript/src/types.ts b/src/Typescript/src/types.ts new file mode 100644 index 0000000..331ebb2 --- /dev/null +++ b/src/Typescript/src/types.ts @@ -0,0 +1,300 @@ +/** + * AuthZen TypeScript Client Types + * + * Type definitions for the AuthZen Authorization API 1.0 specification + */ + +// ============================================================================ +// Core AuthZen Types +// ============================================================================ + +/** + * Subject represents the user or machine principal requesting access + */ +export interface Subject { + type: string; + id: string; + properties?: Record; +} + +/** + * Resource represents the target of the access request + */ +export interface Resource { + type: string; + id: string; + properties?: Record; +} + +/** + * Action represents the operation being attempted on the resource + */ +export interface Action { + name: string; + properties?: Record; +} + +/** + * Context provides environmental and contextual data for the authorization decision + */ +export interface Context { + [key: string]: any; +} + +// ============================================================================ +// Request/Response Types +// ============================================================================ + +/** + * Single access evaluation request + */ +export interface AccessEvaluationRequest { + subject?: Subject; // Make optional - can use defaults + resource?: Resource; // Make optional - can use defaults + action?: Action; // Make optional - can use defaults + context?: Context; +} + +/** + * Single access evaluation response + */ +export interface AccessEvaluationResponse { + decision: boolean; + context?: Context; +} + +/** + * Evaluation semantics for batch requests + */ +export type EvaluationSemantics = 'execute_all' | 'deny_on_first_deny' | 'permit_on_first_permit'; + +/** + * Options for batch evaluation requests + */ +export interface BatchEvaluationOptions { + /** How to handle multiple evaluations */ + evaluations_semantic?: EvaluationSemantics; +} + +/** + * Batch access evaluations request + */ +export interface AccessEvaluationsRequest { + evaluations?: AccessEvaluationRequest[]; // Make optional - can omit for defaults-only + /** Optional evaluation options */ + options?: BatchEvaluationOptions; + /** Default subject values applied to all evaluations */ + subject?: Partial; + /** Default resource values applied to all evaluations */ + resource?: Partial; + /** Default action values applied to all evaluations */ + action?: Partial; + /** Default context values applied to all evaluations */ + context?: Context; +} + +/** + * Individual evaluation result in batch response + */ +export interface EvaluationResult { + decision: boolean; + context?: Context; +} + +/** + * Batch access evaluations response + */ +export interface AccessEvaluationsResponse { + evaluations: EvaluationResult[]; +} + +// ============================================================================ +// Client Configuration +// ============================================================================ + +/** + * Configuration options for the AuthZen client + */ +export interface AuthZenClientConfig { + /** Base URL of the Policy Decision Point (PDP) */ + pdpUrl: string; + /** Optional authentication token (will be sent as Bearer token) */ + token?: string; + /** Additional headers to include with requests */ + headers?: Record; + /** Request timeout in milliseconds (default: 10000) */ + timeout?: number; +} + +/** + * Options for evaluation requests + */ +export interface EvaluationOptions { + /** Custom headers for this request */ + headers?: Record; + /** Override default timeout for this request */ + timeout?: number; +} + +// ============================================================================ +// Error Types +// ============================================================================ + +/** + * Base AuthZen error class + */ +export class AuthZenError extends Error { + public name = 'AuthZenError'; + public readonly requestId?: string; + + constructor(message: string, requestId?: string) { + super(message); + this.requestId = requestId; + + // Ensure proper prototype chain for instanceof checks + Object.setPrototypeOf(this, AuthZenError.prototype); + } +} + +/** + * Network-related errors (timeouts, connection failures, etc.) + */ +export class AuthZenNetworkError extends AuthZenError { + public readonly name = 'AuthZenNetworkError'; + + constructor(message: string, requestId?: string) { + super(message, requestId); + Object.setPrototypeOf(this, AuthZenNetworkError.prototype); + } +} + +/** + * HTTP request errors (4xx, 5xx status codes) + */ +export class AuthZenRequestError extends AuthZenError { + public readonly name = 'AuthZenRequestError'; + public readonly statusCode: number; + public readonly responseData?: any; + + constructor(message: string, statusCode: number, requestId?: string, responseData?: any) { + super(message, requestId); + this.statusCode = statusCode; + this.responseData = responseData; + Object.setPrototypeOf(this, AuthZenRequestError.prototype); + } +} + +/** + * Response parsing/format errors + */ +export class AuthZenResponseError extends AuthZenError { + public readonly name = 'AuthZenResponseError'; + public readonly statusCode: number; + + constructor(message: string, statusCode: number, requestId?: string) { + super(message, requestId); + this.statusCode = statusCode; + Object.setPrototypeOf(this, AuthZenResponseError.prototype); + } +} + +/** + * Request validation errors + */ +export class AuthZenValidationError extends AuthZenError { + public readonly name = 'AuthZenValidationError'; + + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, AuthZenValidationError.prototype); + } +} + +// ============================================================================ +// Utility Types and Constants +// ============================================================================ + +/** + * Common subject types + */ +export const SubjectTypes = { + USER: 'user', + SERVICE: 'service', + GROUP: 'group', + ROLE: 'role', +} as const; + +/** + * Common resource types + */ +export const ResourceTypes = { + DOCUMENT: 'document', + API: 'api', + FOLDER: 'folder', + DATABASE: 'database', + SERVICE: 'service', +} as const; + +/** + * Common action names + */ +export const ActionNames = { + READ: 'can_read', + WRITE: 'can_write', + DELETE: 'can_delete', + EXECUTE: 'can_execute', + CREATE: 'can_create', + UPDATE: 'can_update', + VIEW: 'can_view', + EDIT: 'can_edit', +} as const; + +/** + * HTTP status codes commonly used in AuthZen responses + */ +export const HttpStatusCodes = { + OK: 200, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + INTERNAL_SERVER_ERROR: 500, + SERVICE_UNAVAILABLE: 503, +} as const; + +// ============================================================================ +// Discovery Types +// ============================================================================ + +/** + * AuthZen Configuration returned by the discovery endpoint + * As defined in the AuthZen specification + */ +export interface AuthZenConfiguration { + /** The base URL of the Policy Decision Point (required) */ + policy_decision_point: string; + /** Access evaluation endpoint URL (optional) */ + access_evaluation_endpoint?: string; + /** Access evaluations (batch) endpoint URL (optional) */ + access_evaluations_endpoint?: string; + /** Subject search endpoint URL (optional) */ + search_subject_endpoint?: string; + /** Resource search endpoint URL (optional) */ + search_resource_endpoint?: string; + /** Action search endpoint URL (optional) */ + search_action_endpoint?: string; + /** Additional custom endpoints (optional) */ + [key: string]: string | undefined; +} + +/** + * Error thrown when discovery configuration is invalid + */ +export class AuthZenDiscoveryError extends AuthZenError { + public readonly name = 'AuthZenDiscoveryError'; + + constructor(message: string, requestId?: string) { + super(message, requestId); + Object.setPrototypeOf(this, AuthZenDiscoveryError.prototype); + } +} diff --git a/src/Typescript/test-harness/index.html b/src/Typescript/test-harness/index.html new file mode 100644 index 0000000..b27fa6d --- /dev/null +++ b/src/Typescript/test-harness/index.html @@ -0,0 +1,1016 @@ + + + + + + AuthZen Client Test Page + + + +
+
+

AuthZen Client Test Page

+

Test your AuthZen client library against a real Policy Decision Point (PDP)

+
+ +
+
+ + + + +
+ + +
+

Client Configuration

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Not Configured + +
+

Example Configurations:

+ + + +
+
+ + +
+

Single Access Evaluation

+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ + + +
+

Example Requests:

+ + + +
+ + +
+ + +
+

Batch Access Evaluations

+ +
+ + +
+ +

Default Values

+
+

Default Subject

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+

Default Resource

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+

Default Action

+
+ + +
+
+ + +
+
+ +
+

Default Context

+
+ + +
+
+ +

Evaluations

+
+ +
+ + + + +
+

Example Batches:

+ + + +
+ + +
+ + +
+

PDP Discovery

+

Discover the configuration and capabilities of your Policy Decision Point.

+ + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/src/Typescript/tsconfig.json b/src/Typescript/tsconfig.json new file mode 100644 index 0000000..a64f166 --- /dev/null +++ b/src/Typescript/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "module": "CommonJS", + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": false, + "exactOptionalPropertyTypes": false + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +} diff --git a/src/Typescript/webpack.config.js b/src/Typescript/webpack.config.js new file mode 100644 index 0000000..f213226 --- /dev/null +++ b/src/Typescript/webpack.config.js @@ -0,0 +1,26 @@ +const path = require('path'); + +module.exports = { + entry: "./src/index.ts", + output: { + path: path.join(__dirname, "dist"), + filename: "index.js", + library: 'AuthZenClient', + libraryTarget: 'window', + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + externals: { + fetch: 'fetch' + }, +}; \ No newline at end of file From 05d00f8cd4e7fd3d672e469a24e665772505c36f Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 1 Sep 2025 15:40:56 +0100 Subject: [PATCH 11/33] Add tests to use discovery doc to resolve evaluate endpoint --- .gitignore | 3 + src/Typescript/src/client.test.ts | 128 ++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/.gitignore b/.gitignore index cd86239..3bac34c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ riderModule.iml **/.DS_Store src/CSharp/.idea/.idea.Rsk.AuthZen/.idea/ + +src/Typescript/dist/ +src/Typescript/node_modules diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts index 5bcf075..6251a55 100644 --- a/src/Typescript/src/client.test.ts +++ b/src/Typescript/src/client.test.ts @@ -94,6 +94,134 @@ describe('AuthZenClient', () => { resource: { type: 'document', id: '123' }, }; + it('should call discover and use the absolute evaluation endpoint from discovery result on first evaluate', async () => { + const mockDiscoveryResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate', // absolute URI + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluateResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ decision: true }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + // First call is discovery, second is evaluate + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse) + .mockResolvedValueOnce(mockEvaluateResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + const response = await client.evaluate(validRequest); + + // First call: discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer test-token', + 'X-Request-ID': expect.stringMatching(/^authzen-\d+-[a-z0-9]+$/), + }), + signal: expect.any(AbortSignal), + }) + ); + + // Second call: evaluate using absolute endpoint from discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluate', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(validRequest), + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer test-token', + 'X-Request-ID': expect.stringMatching(/^authzen-\d+-[a-z0-9]+$/), + }), + signal: expect.any(AbortSignal), + }) + ); + + expect(response).toEqual({ decision: true }); + }); + + it('should use cached discovery result for subsequent evaluate calls', async () => { + const mockDiscoveryResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate', // absolute URI + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluateResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ decision: true }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + // First call is discovery, then two evaluate calls + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse) + .mockResolvedValue(mockEvaluateResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + const response1 = await client.evaluate(validRequest); + const response2 = await client.evaluate(validRequest); + + // Only one discovery call + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + // Both evaluate calls use the absolute endpoint from discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluate', + expect.anything() + ); + expect(mockFetch).toHaveBeenNthCalledWith( + 3, + 'https://example.com/custom/evaluate', + expect.anything() + ); + + expect(response1).toEqual({ decision: true }); + expect(response2).toEqual({ decision: true }); + }); + it('should make correct API call for access evaluation', async () => { const mockResponse = { ok: true, From b2b9ddbb9fed4fd10f22565dda1e9054fe1c7b86 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 1 Sep 2025 15:43:33 +0100 Subject: [PATCH 12/33] Add tests for 404 handling when calling evaluate --- src/Typescript/src/client.test.ts | 167 ++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts index 6251a55..255b242 100644 --- a/src/Typescript/src/client.test.ts +++ b/src/Typescript/src/client.test.ts @@ -222,6 +222,173 @@ describe('AuthZenClient', () => { expect(response2).toEqual({ decision: true }); }); + it('should retry discovery and evaluation once if evaluation endpoint returns 404', async () => { + const mockDiscoveryResponse1 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockDiscoveryResponse2 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate2', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mock404Response = { + ok: false, + status: 404, + statusText: 'Not Found', + json: jest.fn().mockResolvedValue({ message: 'Not Found' }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluateResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ decision: true }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + // Sequence: discover, 404, discover, success + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse1) // initial discover + .mockResolvedValueOnce(mock404Response) // first evaluate (404) + .mockResolvedValueOnce(mockDiscoveryResponse2) // retry discover + .mockResolvedValueOnce(mockEvaluateResponse); // retry evaluate (success) + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + const response = await client.evaluate(validRequest); + + // First call: initial discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + // Second call: first evaluate (404) + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluate', + expect.anything() + ); + + // Third call: retry discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 3, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + // Fourth call: retry evaluate (success) + expect(mockFetch).toHaveBeenNthCalledWith( + 4, + 'https://example.com/custom/evaluate2', + expect.anything() + ); + + expect(response).toEqual({ decision: true }); + }); + + it('should return error if evaluation endpoint returns 404 twice', async () => { + const mockDiscoveryResponse1 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockDiscoveryResponse2 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate2', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mock404Response = { + ok: false, + status: 404, + statusText: 'Not Found', + json: jest.fn().mockResolvedValue({ message: 'Not Found' }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + // Sequence: discover, 404, discover, 404 + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse1) // initial discover + .mockResolvedValueOnce(mock404Response) // first evaluate (404) + .mockResolvedValueOnce(mockDiscoveryResponse2) // retry discover + .mockResolvedValueOnce(mock404Response); // retry evaluate (404) + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + await expect(client.evaluate(validRequest)).rejects.toThrow(AuthZenRequestError); + + // First call: initial discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + // Second call: first evaluate (404) + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluate', + expect.anything() + ); + + // Third call: retry discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 3, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + // Fourth call: retry evaluate (404) + expect(mockFetch).toHaveBeenNthCalledWith( + 4, + 'https://example.com/custom/evaluate2', + expect.anything() + ); + }); + it('should make correct API call for access evaluation', async () => { const mockResponse = { ok: true, From ad1c0659231e6b03a9f38445a4238f51b1db1272 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 1 Sep 2025 15:48:36 +0100 Subject: [PATCH 13/33] Add discovery & 404 tests for evaluations endpoint --- src/Typescript/src/client.test.ts | 275 ++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts index 255b242..44ce230 100644 --- a/src/Typescript/src/client.test.ts +++ b/src/Typescript/src/client.test.ts @@ -558,6 +558,281 @@ describe('AuthZenClient', () => { ], }; + it('should call discover and use the absolute evaluations endpoint from discovery result on first evaluations', async () => { + const mockDiscoveryResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluations_endpoint: 'https://example.com/custom/evaluations', // absolute URI + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluationsResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ evaluations: [{ decision: true }, { decision: false }] }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse) + .mockResolvedValueOnce(mockEvaluationsResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + const response = await client.evaluations(validRequest); + + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluations', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(validRequest), + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer test-token', + 'X-Request-ID': expect.stringMatching(/^authzen-\d+-[a-z0-9]+$/), + }), + signal: expect.any(AbortSignal), + }) + ); + + expect(response).toEqual({ evaluations: [{ decision: true }, { decision: false }] }); + }); + + it('should use cached discovery result for subsequent evaluations calls', async () => { + const mockDiscoveryResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluations_endpoint: 'https://example.com/custom/evaluations', // absolute URI + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluationsResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ evaluations: [{ decision: true }, { decision: false }] }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + // First call is discovery, then two evaluate calls + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse) + .mockResolvedValue(mockEvaluationsResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + const response1 = await client.evaluations(validRequest); + const response2 = await client.evaluations(validRequest); + + // Only one discovery call + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + // Both evaluate calls use the absolute endpoint from discovery + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluations', + expect.anything() + ); + expect(mockFetch).toHaveBeenNthCalledWith( + 3, + 'https://example.com/custom/evaluations', + expect.anything() + ); + + expect(response1).toEqual({ evaluations: [{ decision: true }, { decision: false }] }); + expect(response2).toEqual({ evaluations: [{ decision: true }, { decision: false }] }); + }); + + it('should retry discovery and evaluations once if evaluations endpoint returns 404', async () => { + const mockDiscoveryResponse1 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluations_endpoint: 'https://example.com/custom/evaluations', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockDiscoveryResponse2 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluations_endpoint: 'https://example.com/custom/evaluations2', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mock404Response = { + ok: false, + status: 404, + statusText: 'Not Found', + json: jest.fn().mockResolvedValue({ message: 'Not Found' }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluationsResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ evaluations: [{ decision: true }, { decision: false }] }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + // Sequence: discover, 404, discover, success + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse1) // initial discover + .mockResolvedValueOnce(mock404Response) // first evaluations (404) + .mockResolvedValueOnce(mockDiscoveryResponse2) // retry discover + .mockResolvedValueOnce(mockEvaluationsResponse); // retry evaluations (success) + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + const response = await client.evaluations(validRequest); + + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluations', + expect.anything() + ); + + expect(mockFetch).toHaveBeenNthCalledWith( + 3, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + expect(mockFetch).toHaveBeenNthCalledWith( + 4, + 'https://example.com/custom/evaluations2', + expect.anything() + ); + + expect(response).toEqual({ evaluations: [{ decision: true }, { decision: false }] }); + }); + + it('should return error if evaluations endpoint returns 404 twice', async () => { + const mockDiscoveryResponse1 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluations_endpoint: 'https://example.com/custom/evaluations', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockDiscoveryResponse2 = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluations_endpoint: 'https://example.com/custom/evaluations2', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mock404Response = { + ok: false, + status: 404, + statusText: 'Not Found', + json: jest.fn().mockResolvedValue({ message: 'Not Found' }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + // Sequence: discover, 404, discover, 404 + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse1) // initial discover + .mockResolvedValueOnce(mock404Response) // first evaluations (404) + .mockResolvedValueOnce(mockDiscoveryResponse2) // retry discover + .mockResolvedValueOnce(mock404Response); // retry evaluations (404) + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'test-token', + }); + + await expect(client.evaluations(validRequest)).rejects.toThrow(AuthZenRequestError); + + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluations', + expect.anything() + ); + + expect(mockFetch).toHaveBeenNthCalledWith( + 3, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + + expect(mockFetch).toHaveBeenNthCalledWith( + 4, + 'https://example.com/custom/evaluations2', + expect.anything() + ); + }); + it('should make correct API call for batch evaluation', async () => { const mockResponse = { ok: true, From 4f332d69009818901fb1552a25ccd3aa2f92a2a8 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 1 Sep 2025 15:52:17 +0100 Subject: [PATCH 14/33] Update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3bac34c..ca8de4d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ src/CSharp/.idea/.idea.Rsk.AuthZen/.idea/ src/Typescript/dist/ src/Typescript/node_modules + +src/CSharp/.idea/ From 90f573ece1d0a1aff951dff87aa553cfd3dc7c1c Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 1 Sep 2025 16:01:56 +0100 Subject: [PATCH 15/33] Update setup of other tests --- src/Typescript/src/client.test.ts | 70 ++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts index 44ce230..0d68439 100644 --- a/src/Typescript/src/client.test.ts +++ b/src/Typescript/src/client.test.ts @@ -24,6 +24,23 @@ describe('AuthZenClient', () => { jest.useRealTimers(); }); + // Helper to mock discovery before each test that expects an API call + const setupDiscovery = (overrides = {}) => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/access/v1/evaluation', + access_evaluations_endpoint: 'https://example.com/access/v1/evaluations', + ...overrides, + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }); + }; + describe('constructor', () => { it('should create a client with required config', () => { const client = new AuthZenClient({ @@ -390,6 +407,7 @@ describe('AuthZenClient', () => { }); it('should make correct API call for access evaluation', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -407,7 +425,13 @@ describe('AuthZenClient', () => { const response = await client.evaluate(validRequest); - expect(mockFetch).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + expect(mockFetch).toHaveBeenNthCalledWith( + 2, 'https://example.com/access/v1/evaluation', expect.objectContaining({ method: 'POST', @@ -426,6 +450,7 @@ describe('AuthZenClient', () => { }); it('should handle HTTP errors', async () => { + setupDiscovery(); const mockResponse = { ok: false, status: 403, @@ -445,6 +470,7 @@ describe('AuthZenClient', () => { }); it('should handle network errors', async () => { + setupDiscovery(); mockFetch.mockRejectedValue(new Error('Network error')); const client = new AuthZenClient({ @@ -455,6 +481,7 @@ describe('AuthZenClient', () => { }); it('should handle timeout errors', async () => { + setupDiscovery(); // Create a promise that will be rejected with AbortError when signal is aborted mockFetch.mockImplementation((url, options) => { return new Promise((resolve, reject) => { @@ -491,6 +518,7 @@ describe('AuthZenClient', () => { }); it('should handle invalid JSON responses', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -509,6 +537,7 @@ describe('AuthZenClient', () => { }); it('should handle non-JSON responses', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -527,6 +556,7 @@ describe('AuthZenClient', () => { }); it('should validate request before sending', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -834,6 +864,7 @@ describe('AuthZenClient', () => { }); it('should make correct API call for batch evaluation', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -852,7 +883,13 @@ describe('AuthZenClient', () => { const response = await client.evaluations(validRequest); - expect(mockFetch).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://example.com/.well-known/authzen-configuration', + expect.anything() + ); + expect(mockFetch).toHaveBeenNthCalledWith( + 2, 'https://example.com/access/v1/evaluations', expect.objectContaining({ method: 'POST', @@ -872,6 +909,7 @@ describe('AuthZenClient', () => { }); it('should validate evaluations_semantic in options', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -889,6 +927,7 @@ describe('AuthZenClient', () => { }); it('should accept valid evaluations_semantic values', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -923,6 +962,7 @@ describe('AuthZenClient', () => { }); it('should allow request without options', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -949,6 +989,7 @@ describe('AuthZenClient', () => { }); it('should allow empty options object', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -975,6 +1016,7 @@ describe('AuthZenClient', () => { }); it('should validate that options is an object if provided', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1003,6 +1045,7 @@ describe('AuthZenClient', () => { }); it('should validate individual evaluations in batch', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1027,6 +1070,7 @@ describe('AuthZenClient', () => { }); it('should validate default values structure', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -1064,6 +1108,7 @@ describe('AuthZenClient', () => { describe('default value handling', () => { it('should allow missing subject in evaluations when default subject is provided', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -1101,6 +1146,7 @@ describe('AuthZenClient', () => { }); it('should allow missing resource in evaluations when default resource is provided', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -1138,6 +1184,7 @@ describe('AuthZenClient', () => { }); it('should allow missing action in evaluations when default action is provided', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -1175,6 +1222,7 @@ describe('AuthZenClient', () => { }); it('should allow completely empty evaluations when all defaults are provided', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -1208,6 +1256,7 @@ describe('AuthZenClient', () => { }); it('should still require subject when no default subject is provided', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1231,6 +1280,7 @@ describe('AuthZenClient', () => { }); it('should still require resource when no default resource is provided', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1254,6 +1304,7 @@ describe('AuthZenClient', () => { }); it('should still require action when no default action is provided', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1279,6 +1330,7 @@ describe('AuthZenClient', () => { describe('partial element validation', () => { it('should reject partial subject (missing type) in evaluations', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1299,6 +1351,7 @@ describe('AuthZenClient', () => { }); it('should reject partial subject (missing id) in evaluations', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1339,6 +1392,7 @@ describe('AuthZenClient', () => { }); it('should reject partial resource (missing id) in evaluations', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1359,6 +1413,7 @@ describe('AuthZenClient', () => { }); it('should reject partial action (missing name) in evaluations', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1379,6 +1434,7 @@ describe('AuthZenClient', () => { }); it('should reject partial subject in defaults when used without evaluations', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1417,6 +1473,7 @@ describe('AuthZenClient', () => { }); it('should reject partial action in defaults when used without evaluations', async () => { + setupDiscovery(); const client = new AuthZenClient({ pdpUrl: 'https://example.com', }); @@ -1436,6 +1493,7 @@ describe('AuthZenClient', () => { }); it('should allow complete elements with properties', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, @@ -1488,6 +1546,7 @@ describe('AuthZenClient', () => { }); it('should validate subject type is required', async () => { + setupDiscovery(); const invalidRequest = { subject: { id: 'alice@example.com' }, // Missing type action: { name: 'can_read' }, @@ -1498,6 +1557,7 @@ describe('AuthZenClient', () => { }); it('should validate subject id is required', async () => { + setupDiscovery(); const invalidRequest = { subject: { type: 'user' }, // Missing id action: { name: 'can_read' }, @@ -1508,6 +1568,7 @@ describe('AuthZenClient', () => { }); it('should validate resource type is required', async () => { + setupDiscovery(); const invalidRequest = { subject: { type: 'user', id: 'alice@example.com' }, action: { name: 'can_read' }, @@ -1518,6 +1579,7 @@ describe('AuthZenClient', () => { }); it('should validate resource id is required', async () => { + setupDiscovery(); const invalidRequest = { subject: { type: 'user', id: 'alice@example.com' }, action: { name: 'can_read' }, @@ -1528,6 +1590,7 @@ describe('AuthZenClient', () => { }); it('should validate action name is required', async () => { + setupDiscovery(); const invalidRequest = { subject: { type: 'user', id: 'alice@example.com' }, action: {}, // Missing name @@ -1548,6 +1611,7 @@ describe('AuthZenClient', () => { }); it('should wrap unknown errors as AuthZenError', async () => { + setupDiscovery(); mockFetch.mockRejectedValue(new Error('Unknown error')); const validRequest: AccessEvaluationRequest = { @@ -1560,6 +1624,7 @@ describe('AuthZenClient', () => { }); it('should preserve AuthZen errors', async () => { + setupDiscovery(); const customError = new AuthZenValidationError('Custom validation error'); // Mock the validation to throw our custom error @@ -1579,6 +1644,7 @@ describe('AuthZenClient', () => { describe('request ID generation', () => { it('should generate unique request IDs', async () => { + setupDiscovery(); const mockResponse = { ok: true, status: 200, From ba2306fe0c6f602e53fcfeb3b3d6af4a8b2a2a63 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 1 Sep 2025 16:56:35 +0100 Subject: [PATCH 16/33] Update client --- src/Typescript/src/client.ts | 87 ++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/src/Typescript/src/client.ts b/src/Typescript/src/client.ts index a83cdc2..9fa9067 100644 --- a/src/Typescript/src/client.ts +++ b/src/Typescript/src/client.ts @@ -30,6 +30,7 @@ export class AuthZenClient implements IAuthZenClient { readonly pdpUrl: string; private readonly headers: Record; private readonly timeout: number; + private _discoveryCache?: AuthZenConfiguration; constructor(config: AuthZenClientConfig) { this.pdpUrl = config.pdpUrl.replace(/\/$/, ''); // Remove trailing slash @@ -63,7 +64,7 @@ export class AuthZenClient implements IAuthZenClient { const config = response as AuthZenConfiguration; this.validateDiscoveryConfiguration(config); - + this._discoveryCache = config; return config; } catch (error) { if (error instanceof AuthZenError) { @@ -73,20 +74,56 @@ export class AuthZenClient implements IAuthZenClient { } } + /** + * Get cached discovery configuration or fetch new + */ + private async getDiscovery(): Promise { + if (this._discoveryCache) return this._discoveryCache; + return await this.discover(); + } + /** * Make a single access evaluation request */ async evaluate(request: AccessEvaluationRequest): Promise { this.validateEvaluationRequest(request); + let discovery = await this.getDiscovery(); + let endpoint = discovery.access_evaluation_endpoint; + if (!endpoint || typeof endpoint !== 'string') { + throw new AuthZenDiscoveryError('Discovery configuration missing access_evaluation_endpoint'); + } + + // Always use absolute URI from discovery + let url = endpoint; + try { - const response = await this.makeRequest('/access/v1/evaluation', { + const response = await this.makeRequest(url, { method: 'POST', body: JSON.stringify(request), + absolute: true, }); - return response as AccessEvaluationResponse; - } catch (error) { + } catch (error: any) { + // If 404, reacquire discovery and retry once + if (error instanceof AuthZenRequestError && error.statusCode === 404) { + discovery = await this.discover(); + endpoint = discovery.access_evaluation_endpoint; + if (!endpoint || typeof endpoint !== 'string') { + throw new AuthZenDiscoveryError('Discovery configuration missing access_evaluation_endpoint'); + } + url = endpoint; + try { + const response = await this.makeRequest(url, { + method: 'POST', + body: JSON.stringify(request), + absolute: true, + }); + return response as AccessEvaluationResponse; + } catch (err: any) { + throw this.handleError(err, 'evaluate'); + } + } throw this.handleError(error, 'evaluate'); } } @@ -97,23 +134,49 @@ export class AuthZenClient implements IAuthZenClient { async evaluations(request: AccessEvaluationsRequest): Promise { this.validateEvaluationsRequest(request); + let discovery = await this.getDiscovery(); + let endpoint = discovery.access_evaluations_endpoint; + if (!endpoint || typeof endpoint !== 'string') { + throw new AuthZenDiscoveryError('Discovery configuration missing access_evaluations_endpoint'); + } + + let url = endpoint; + try { - const response = await this.makeRequest('/access/v1/evaluations', { + const response = await this.makeRequest(url, { method: 'POST', body: JSON.stringify(request), + absolute: true, }); - return response as AccessEvaluationsResponse; - } catch (error) { + } catch (error: any) { + if (error instanceof AuthZenRequestError && error.statusCode === 404) { + discovery = await this.discover(); + endpoint = discovery.access_evaluations_endpoint; + if (!endpoint || typeof endpoint !== 'string') { + throw new AuthZenDiscoveryError('Discovery configuration missing access_evaluations_endpoint'); + } + url = endpoint; + try { + const response = await this.makeRequest(url, { + method: 'POST', + body: JSON.stringify(request), + absolute: true, + }); + return response as AccessEvaluationsResponse; + } catch (err: any) { + throw this.handleError(err, 'evaluations'); + } + } throw this.handleError(error, 'evaluations'); } } /** - * Make an HTTP request to the PDP + * Make an HTTP request to the PDP or absolute endpoint */ - private async makeRequest(endpoint: string, options: RequestInit): Promise { - const url = `${this.pdpUrl}${endpoint}`; + private async makeRequest(endpoint: string, options: RequestInit & { absolute?: boolean }): Promise { + const url = options.absolute ? endpoint : `${this.pdpUrl}${endpoint}`; const requestId = this.generateRequestId(); // Create abort controller for timeout handling @@ -133,16 +196,13 @@ export class AuthZenClient implements IAuthZenClient { }; let response: Response; - try { response = await fetch(url, requestOptions); } catch (error: any) { clearTimeout(timeoutId); - if (error.name === 'AbortError') { throw new AuthZenNetworkError(`Request timeout after ${this.timeout}ms`, requestId); } - throw new AuthZenNetworkError(`Network error: ${error.message}`, requestId); } finally { clearTimeout(timeoutId); @@ -151,7 +211,6 @@ export class AuthZenClient implements IAuthZenClient { // Handle non-JSON responses let responseData: any; const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { try { responseData = await response.json(); From 11af4183c5ae8d95f91250c69c00a33e76a8674f Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 2 Sep 2025 15:15:34 +0100 Subject: [PATCH 17/33] Fix tests w.r.t. discovery call --- src/Typescript/package-lock.json | 2115 ++++++++++++++++++++++++-- src/Typescript/package.json | 6 +- src/Typescript/src/client.test.ts | 114 +- src/Typescript/src/client.ts | 14 +- src/Typescript/src/discovery.test.ts | 24 - 5 files changed, 2008 insertions(+), 265 deletions(-) diff --git a/src/Typescript/package-lock.json b/src/Typescript/package-lock.json index f6a6ba7..2e2b3b6 100644 --- a/src/Typescript/package-lock.json +++ b/src/Typescript/package-lock.json @@ -9,9 +9,13 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@types/jest": "^29.5.12", + "@babel/core": "^7.28.3", + "@babel/preset-env": "^7.28.3", + "@babel/preset-typescript": "^7.27.1", + "@types/jest": "^29.5.14", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", + "babel-jest": "^30.1.2", "eslint": "^8.57.0", "jest": "^29.7.0", "ts-jest": "^29.2.4", @@ -61,22 +65,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -102,14 +106,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -118,6 +122,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", @@ -145,6 +162,83 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -155,6 +249,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -170,15 +278,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -187,6 +295,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", @@ -197,6 +318,56 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -227,10 +398,25 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { @@ -242,13 +428,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -257,6 +443,103 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -312,6 +595,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", @@ -354,10 +653,910 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-jsx": { + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", + "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", + "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", + "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", + "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, "license": "MIT", "dependencies": { @@ -370,108 +1569,202 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "node_modules/@babel/preset-env": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -480,14 +1773,43 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-typescript": { + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -512,18 +1834,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -970,6 +2292,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -1950,76 +3296,301 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", + "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.1.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-jest/node_modules/@jest/transform": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", + "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-jest/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-jest/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest/node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-jest/node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/jest-haste-map": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/babel-jest/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-jest/node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/babel-jest/node_modules/jest-worker": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-jest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/babel-jest/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/babel-jest/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "node_modules/babel-jest/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/babel-plugin-istanbul": { @@ -2067,19 +3638,70 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-preset-current-node-syntax": { @@ -2110,20 +3732,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.11.0" } }, "node_modules/balanced-match": { @@ -2157,9 +3779,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -2177,8 +3799,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2240,9 +3862,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "dev": true, "funding": [ { @@ -2416,6 +4038,20 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js-compat": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2550,9 +4186,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.199", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", - "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", + "version": "1.5.211", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", + "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", "dev": true, "license": "ISC" }, @@ -3759,6 +5395,61 @@ } } }, + "node_modules/jest-config/node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -4349,6 +6040,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4948,6 +6646,77 @@ "node": ">= 10.13.0" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5750,6 +7519,50 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/src/Typescript/package.json b/src/Typescript/package.json index d849517..ff88f1b 100644 --- a/src/Typescript/package.json +++ b/src/Typescript/package.json @@ -23,9 +23,13 @@ "author": "Your Name", "license": "MIT", "devDependencies": { - "@types/jest": "^29.5.12", + "@babel/core": "^7.28.3", + "@babel/preset-env": "^7.28.3", + "@babel/preset-typescript": "^7.27.1", + "@types/jest": "^29.5.14", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", + "babel-jest": "^30.1.2", "eslint": "^8.57.0", "jest": "^29.7.0", "ts-jest": "^29.2.4", diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts index 0d68439..52cb194 100644 --- a/src/Typescript/src/client.test.ts +++ b/src/Typescript/src/client.test.ts @@ -15,7 +15,7 @@ global.fetch = mockFetch; describe('AuthZenClient', () => { beforeEach(() => { - mockFetch.mockClear(); + mockFetch.mockReset(); jest.clearAllTimers(); jest.useFakeTimers(); }); @@ -31,14 +31,14 @@ describe('AuthZenClient', () => { status: 200, json: jest.fn().mockResolvedValue({ policy_decision_point: 'https://example.com', - access_evaluation_endpoint: 'https://example.com/access/v1/evaluation', - access_evaluations_endpoint: 'https://example.com/access/v1/evaluations', + access_evaluation_endpoint: 'https://example.com/setupDiscovery/evaluation', + access_evaluations_endpoint: 'https://example.com/setupDiscovery/evaluations', ...overrides, }), headers: { get: jest.fn().mockReturnValue('application/json') }, - }); + }); }; describe('constructor', () => { @@ -59,49 +59,49 @@ describe('AuthZenClient', () => { expect(client.pdpUrl).toBe('https://example.com'); }); - it('should set default timeout to 10 seconds', () => { - const client = new AuthZenClient({ - pdpUrl: 'https://example.com', - }); + // it('should set default timeout to 10 seconds', () => { + // const client = new AuthZenClient({ + // pdpUrl: 'https://example.com', + // }); - // We can't directly access timeout, but we can test it through behavior - expect(client).toBeInstanceOf(AuthZenClient); + // // We can't directly access timeout, but we can test it through behavior + // expect(client).toBeInstanceOf(AuthZenClient); - // Todo - }); + // // Todo + // }); - it('should set custom timeout when provided', () => { - const client = new AuthZenClient({ - pdpUrl: 'https://example.com', - timeout: 5000, - }); + // it('should set custom timeout when provided', () => { + // const client = new AuthZenClient({ + // pdpUrl: 'https://example.com', + // timeout: 5000, + // }); - expect(client).toBeInstanceOf(AuthZenClient); + // expect(client).toBeInstanceOf(AuthZenClient); - // Todo - }); + // // Todo + // }); - it('should set Authorization header when token provided', () => { - const client = new AuthZenClient({ - pdpUrl: 'https://example.com', - token: 'test-token', - }); + // it('should set Authorization header when token provided', () => { + // const client = new AuthZenClient({ + // pdpUrl: 'https://example.com', + // token: 'test-token', + // }); - expect(client).toBeInstanceOf(AuthZenClient); + // expect(client).toBeInstanceOf(AuthZenClient); - // Todo - }); + // // Todo + // }); - it('should merge custom headers', () => { - const client = new AuthZenClient({ - pdpUrl: 'https://example.com', - headers: { 'Custom-Header': 'test-value' }, - }); + // it('should merge custom headers', () => { + // const client = new AuthZenClient({ + // pdpUrl: 'https://example.com', + // headers: { 'Custom-Header': 'test-value' }, + // }); - expect(client).toBeInstanceOf(AuthZenClient); + // expect(client).toBeInstanceOf(AuthZenClient); - // Todo - }); + // // Todo + // }); }); describe('evaluate', () => { @@ -432,7 +432,7 @@ describe('AuthZenClient', () => { ); expect(mockFetch).toHaveBeenNthCalledWith( 2, - 'https://example.com/access/v1/evaluation', + 'https://example.com/setupDiscovery/evaluation', expect.objectContaining({ method: 'POST', body: JSON.stringify(validRequest), @@ -480,43 +480,6 @@ describe('AuthZenClient', () => { await expect(client.evaluate(validRequest)).rejects.toThrow(AuthZenNetworkError); }); - it('should handle timeout errors', async () => { - setupDiscovery(); - // Create a promise that will be rejected with AbortError when signal is aborted - mockFetch.mockImplementation((url, options) => { - return new Promise((resolve, reject) => { - // Set up abort signal listener - if (options?.signal) { - const abortHandler = () => { - const error = new Error('The operation was aborted'); - error.name = 'AbortError'; - reject(error); - }; - - options.signal.addEventListener('abort', abortHandler); - - // Clean up listener if request completes normally - options.signal.addEventListener('abort', () => { - options.signal.removeEventListener('abort', abortHandler); - }); - } - }); - }); - - const client = new AuthZenClient({ - pdpUrl: 'https://example.com', - timeout: 1000, - }); - - const evaluatePromise = client.evaluate(validRequest); - - // Advance time to trigger timeout - jest.advanceTimersByTime(1100); // Slightly more than timeout - - await expect(evaluatePromise).rejects.toThrow(AuthZenNetworkError); - await expect(evaluatePromise).rejects.toThrow('Request timeout after 1000ms'); - }); - it('should handle invalid JSON responses', async () => { setupDiscovery(); const mockResponse = { @@ -890,7 +853,7 @@ describe('AuthZenClient', () => { ); expect(mockFetch).toHaveBeenNthCalledWith( 2, - 'https://example.com/access/v1/evaluations', + 'https://example.com/setupDiscovery/evaluations', expect.objectContaining({ method: 'POST', body: JSON.stringify(validRequest), @@ -1611,7 +1574,6 @@ describe('AuthZenClient', () => { }); it('should wrap unknown errors as AuthZenError', async () => { - setupDiscovery(); mockFetch.mockRejectedValue(new Error('Unknown error')); const validRequest: AccessEvaluationRequest = { diff --git a/src/Typescript/src/client.ts b/src/Typescript/src/client.ts index 9fa9067..1d21df3 100644 --- a/src/Typescript/src/client.ts +++ b/src/Typescript/src/client.ts @@ -179,12 +179,6 @@ export class AuthZenClient implements IAuthZenClient { const url = options.absolute ? endpoint : `${this.pdpUrl}${endpoint}`; const requestId = this.generateRequestId(); - // Create abort controller for timeout handling - const abortController = new AbortController(); - const timeoutId = setTimeout(() => { - abortController.abort(); - }, this.timeout); - const requestOptions: RequestInit = { ...options, headers: { @@ -192,20 +186,14 @@ export class AuthZenClient implements IAuthZenClient { 'X-Request-ID': requestId, ...options.headers, }, - signal: abortController.signal, + signal: AbortSignal.timeout(this.timeout), }; let response: Response; try { response = await fetch(url, requestOptions); } catch (error: any) { - clearTimeout(timeoutId); - if (error.name === 'AbortError') { - throw new AuthZenNetworkError(`Request timeout after ${this.timeout}ms`, requestId); - } throw new AuthZenNetworkError(`Network error: ${error.message}`, requestId); - } finally { - clearTimeout(timeoutId); } // Handle non-JSON responses diff --git a/src/Typescript/src/discovery.test.ts b/src/Typescript/src/discovery.test.ts index ea383a9..bd9b344 100644 --- a/src/Typescript/src/discovery.test.ts +++ b/src/Typescript/src/discovery.test.ts @@ -377,30 +377,6 @@ describe('AuthZenClient - Discovery', () => { }); describe('network errors', () => { - it('should handle network timeouts', async () => { - // Mock fetch to simulate hanging request - mockFetch.mockImplementation((url, options) => { - return new Promise((resolve, reject) => { - // Set up abort signal listener - if (options?.signal) { - const abortHandler = () => { - const error = new Error('The operation was aborted'); - error.name = 'AbortError'; - reject(error); - }; - - options.signal.addEventListener('abort', abortHandler); - } - }); - }); - - const promise = client.discover(); - jest.advanceTimersByTime(10000); - - await expect(promise).rejects.toThrow(AuthZenNetworkError); - await expect(promise).rejects.toThrow('Request timeout after 10000ms'); - }); - it('should handle connection errors', async () => { mockFetch.mockRejectedValue(new Error('Connection refused')); From 88800376323f93aa6040759945cff9824d029956 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 2 Sep 2025 15:30:46 +0100 Subject: [PATCH 18/33] Update webpack config --- src/Typescript/src/client.test.ts | 22 ---------------------- src/Typescript/webpack.config.js | 6 ++++-- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts index 52cb194..675aada 100644 --- a/src/Typescript/src/client.test.ts +++ b/src/Typescript/src/client.test.ts @@ -59,28 +59,6 @@ describe('AuthZenClient', () => { expect(client.pdpUrl).toBe('https://example.com'); }); - // it('should set default timeout to 10 seconds', () => { - // const client = new AuthZenClient({ - // pdpUrl: 'https://example.com', - // }); - - // // We can't directly access timeout, but we can test it through behavior - // expect(client).toBeInstanceOf(AuthZenClient); - - // // Todo - // }); - - // it('should set custom timeout when provided', () => { - // const client = new AuthZenClient({ - // pdpUrl: 'https://example.com', - // timeout: 5000, - // }); - - // expect(client).toBeInstanceOf(AuthZenClient); - - // // Todo - // }); - // it('should set Authorization header when token provided', () => { // const client = new AuthZenClient({ // pdpUrl: 'https://example.com', diff --git a/src/Typescript/webpack.config.js b/src/Typescript/webpack.config.js index f213226..5076820 100644 --- a/src/Typescript/webpack.config.js +++ b/src/Typescript/webpack.config.js @@ -5,8 +5,10 @@ module.exports = { output: { path: path.join(__dirname, "dist"), filename: "index.js", - library: 'AuthZenClient', - libraryTarget: 'window', + library: { + 'name': 'AuthZenClient', + 'type': 'window' + } }, module: { rules: [ From af7555084d155cc50c9b99a5bf72cd95c8e062a0 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 2 Sep 2025 16:00:03 +0100 Subject: [PATCH 19/33] Replace incomplete tests --- src/Typescript/src/client.test.ts | 117 ++++++++++++++++++++++----- src/Typescript/src/discovery.test.ts | 35 ++++++++ 2 files changed, 130 insertions(+), 22 deletions(-) diff --git a/src/Typescript/src/client.test.ts b/src/Typescript/src/client.test.ts index 675aada..ea362d1 100644 --- a/src/Typescript/src/client.test.ts +++ b/src/Typescript/src/client.test.ts @@ -58,28 +58,6 @@ describe('AuthZenClient', () => { expect(client.pdpUrl).toBe('https://example.com'); }); - - // it('should set Authorization header when token provided', () => { - // const client = new AuthZenClient({ - // pdpUrl: 'https://example.com', - // token: 'test-token', - // }); - - // expect(client).toBeInstanceOf(AuthZenClient); - - // // Todo - // }); - - // it('should merge custom headers', () => { - // const client = new AuthZenClient({ - // pdpUrl: 'https://example.com', - // headers: { 'Custom-Header': 'test-value' }, - // }); - - // expect(client).toBeInstanceOf(AuthZenClient); - - // // Todo - // }); }); describe('evaluate', () => { @@ -511,6 +489,54 @@ describe('AuthZenClient', () => { await expect(client.evaluate(invalidRequest)).rejects.toThrow(AuthZenValidationError); expect(mockFetch).not.toHaveBeenCalled(); }); + + it('should include Authorization header with bearer token when provided', async () => { + const mockDiscoveryResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluateResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ decision: true }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse) + .mockResolvedValueOnce(mockEvaluateResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'my-secret-token', + }); + + await client.evaluate(validRequest); + + // Second call: evaluate should include Authorization header + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluate', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'Authorization': 'Bearer my-secret-token', + }), + body: JSON.stringify(validRequest), + signal: expect.any(AbortSignal), + }) + ); + }); }); describe('evaluations', () => { @@ -1045,6 +1071,53 @@ describe('AuthZenClient', () => { await expect(client.evaluations(requestWithDefaults)).resolves.toBeDefined(); expect(mockFetch).toHaveBeenCalled(); }); + + it('should include Authorization header with bearer token when provided', async () => { + const mockDiscoveryResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluations_endpoint: 'https://example.com/custom/evaluations', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + const mockEvaluationsResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ evaluations: [{ decision: true }, { decision: false }] }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + mockFetch + .mockResolvedValueOnce(mockDiscoveryResponse) + .mockResolvedValueOnce(mockEvaluationsResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'my-evaluations-token', + }); + + await client.evaluations(validRequest); + + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://example.com/custom/evaluations', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'Authorization': 'Bearer my-evaluations-token', + }), + body: JSON.stringify(validRequest), + signal: expect.any(AbortSignal), + }) + ); + }); }); describe('default value handling', () => { diff --git a/src/Typescript/src/discovery.test.ts b/src/Typescript/src/discovery.test.ts index bd9b344..7aa3e5e 100644 --- a/src/Typescript/src/discovery.test.ts +++ b/src/Typescript/src/discovery.test.ts @@ -148,6 +148,41 @@ describe('AuthZenClient - Discovery', () => { expect(result).toEqual(mockConfig); }); + + it('should include Authorization header with bearer token when provided', async () => { + const mockDiscoveryResponse = { + ok: true, + status: 200, + json: jest.fn().mockResolvedValue({ + policy_decision_point: 'https://example.com', + access_evaluation_endpoint: 'https://example.com/custom/evaluate', + access_evaluations_endpoint: 'https://example.com/custom/evaluations', + }), + headers: { + get: jest.fn().mockReturnValue('application/json') + }, + }; + + mockFetch.mockResolvedValueOnce(mockDiscoveryResponse); + + const client = new AuthZenClient({ + pdpUrl: 'https://example.com', + token: 'my-discovery-token', + }); + + await client.discover(); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/.well-known/authzen-configuration', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Authorization': 'Bearer my-discovery-token', + }), + signal: expect.any(AbortSignal), + }) + ); + }); }); describe('validation errors', () => { From 1ecc8c1474e496d1b2e12d69a99c720f0dc8c769 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 2 Sep 2025 16:08:37 +0100 Subject: [PATCH 20/33] Removed helper methods from basic-usage.ts --- src/Typescript/examples/basic-usage.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/Typescript/examples/basic-usage.ts b/src/Typescript/examples/basic-usage.ts index c44ed92..1857bd8 100644 --- a/src/Typescript/examples/basic-usage.ts +++ b/src/Typescript/examples/basic-usage.ts @@ -246,26 +246,6 @@ export { runBasicExamples, }; -// Helper functions for creating common objects -export function createUserSubject(id: string, properties?: Record): Subject { - return { type: 'user', id, ...(properties && { properties }) }; -} - -export function createDocumentResource(id: string, properties?: Record): Resource { - return { type: 'document', id, ...(properties && { properties }) }; -} - -export function createAction(name: string, properties?: Record): Action { - return { name, ...(properties && { properties }) }; -} - -export function createContextWithTimestamp(additionalContext?: Record): Context { - return { - time: new Date().toISOString(), - ...additionalContext, - }; -} - // Run examples if this file is executed directly if (require.main === module) { runBasicExamples().catch(console.error); From 552f8cb786ead6710cde68abf735be47dba7ac2c Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Fri, 13 Feb 2026 15:10:22 +0000 Subject: [PATCH 21/33] Enable manual trigger for .NET workflow Added manual trigger for workflow dispatch. --- .github/workflows/dotnet.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 217f7cb..5c9145c 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -4,6 +4,7 @@ name: .NET on: + workflow_dispatch: # Manual trigger push: branches: [ "main" ] pull_request: From 853c6807feefd9d6d212ae1e30fa5174375c2778 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Fri, 13 Feb 2026 15:15:49 +0000 Subject: [PATCH 22/33] Correct .net project path --- .github/workflows/dotnet.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5c9145c..8cd1fb4 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -20,10 +20,10 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore dependencies - run: dotnet restore + run: dotnet restore src/CSharp/Rsk.AuthZen.sln - name: Build - run: dotnet build --no-restore + run: dotnet build src/CSharp/Rsk.AuthZen.sln --no-restore - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test src/CSharp/Rsk.AuthZen.sln --no-build --verbosity normal From 1898e31253091f080d538fe6d4945253ecdde215 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Wed, 18 Feb 2026 11:44:15 +0000 Subject: [PATCH 23/33] Updating actions * Target .net 8 * Include TS build Nuget pack Include licence Versioning nuget publish Fix warnings * missing awaits in test code Typescript build & publish update package json Update webpack config output as module Set version number to 0.0.1 To be set by CI --- .github/workflows/dotnet.yml | 40 +++++++++- .github/workflows/typescript.yml | 75 ++++++++++++++++++ icon.png | Bin 0 -> 6033 bytes .../AuthZenClientTests.cs | 6 +- .../Rsk.AuthZen.Client.csproj | 18 +++++ src/CSharp/Rsk.AuthZen.Client/license.txt | 21 +++++ src/Typescript/package.json | 17 ++-- src/Typescript/webpack.config.js | 14 +++- 8 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/typescript.yml create mode 100644 icon.png create mode 100644 src/CSharp/Rsk.AuthZen.Client/license.txt diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 8cd1fb4..c29053a 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -17,13 +17,47 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Get version from git tag + id: version + run: | + VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.1-preview") + VERSION=${VERSION#v} + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore src/CSharp/Rsk.AuthZen.sln - name: Build - run: dotnet build src/CSharp/Rsk.AuthZen.sln --no-restore + run: dotnet build src/CSharp/Rsk.AuthZen.sln --no-restore --configuration Release - name: Test - run: dotnet test src/CSharp/Rsk.AuthZen.sln --no-build --verbosity normal + run: dotnet test src/CSharp/Rsk.AuthZen.sln --no-build --verbosity normal --configuration Release + - name: Pack NuGet Package + run: dotnet pack src/CSharp/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj --configuration Release --no-build --output ./nupkgs -p:Version=${{ steps.version.outputs.version }} + - name: Upload NuGet Package + uses: actions/upload-artifact@v4 + with: + name: nuget-package + path: ./nupkgs/*.nupkg + + deploy: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + environment: nuget-production + + steps: + - name: Download NuGet Package + uses: actions/download-artifact@v4 + with: + name: nuget-package + path: ./nupkgs + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Push to NuGet.org + run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml new file mode 100644 index 0000000..5e55654 --- /dev/null +++ b/.github/workflows/typescript.yml @@ -0,0 +1,75 @@ +# This workflow will build and test the TypeScript library +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: TypeScript + +on: + workflow_dispatch: # Manual trigger + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Get version from git tag + id: version + run: | + VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.1-preview") + VERSION=${VERSION#v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: src/Typescript/package-lock.json + - name: Install dependencies + run: npm ci + working-directory: src/Typescript + - name: Set package version + run: npm version ${{ steps.version.outputs.version }} --no-git-tag-version --allow-same-version + working-directory: src/Typescript + - name: Build + run: npm run build + working-directory: src/Typescript + - name: Test + run: npm test + working-directory: src/Typescript + - name: Pack npm package + run: npm pack + working-directory: src/Typescript + - name: Upload npm package + uses: actions/upload-artifact@v4 + with: + name: npm-package + path: src/Typescript/*.tgz + + deploy: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + environment: npm-production + + steps: + - name: Download npm package + uses: actions/download-artifact@v4 + with: + name: npm-package + path: ./package + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - name: Publish to npm + run: npm publish ./package/*.tgz --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4dbcd4aeb32db0c79107dd8de134746621019b3d GIT binary patch literal 6033 zcmX|Fby$;K+#cPCC?f>~2^l5fXha<opfKCS^c=Z6@O!F`{581Xw^&TbehjPv~vzmFIl5Qty(&P@ZG;K|MO zRv(cb!Kmn^^FflV7Hh6j;( z_b1;5TJKhIZdLX0swr5_?2dojT?JK6I#fsLmo5x^7I%7&%UeM) zs8&R>GjjCCz#rPoRq?*v-4nOd?ZcxD1S^eRuFIs2=9z6=oT6NPCU4+R;z~=bPP`@0 zz|B#Bvt5(ky@kkO{fH??F^hmrHY&0>2T=5k90mTiYgv!mjn2DVDZ8>kMAL1vCozre ziW(0-ZPMcMd2=IM{>P4P@L}e-(%s`WD;it$_BSG&e^#X0U$y9o8cXd{Wx}~mqm8Ql z6u?n!fGu(1{SJ!loT#;IEQPQ&!#B|;#X?}RuP?=j$F{$LnMu(xa%}UI{%&6IL*dai z*Gs&XvSu%$xq3eyxgQ@b6EjbIKMTR`YVzu$}A}LsBHVL1Hv3 zFjAiqMn+Q=OZzTBv&3n<9@FroxwaktlOm;uinWkjzC|UZF z;voowMg=D0geZ<_ET$<8g1fAUk!?BhXc8e#tH69@>r0L0{Wc~4b#`&2X^xQ#8WLRR zULMt|n0~y%3O3oUc=N%k*((mURWcS+Jj9Cdzn8ByfLxx`ocM(KhNb$VSi#M)_d-9c zJ9G=jXx%6<sce@BpBy1}EicC|Xd@nJKTL_pN#8%&)tnnN6O4vy$85D})iFlR zSkMHAsdVinvyS<7wTo@vUX9LPaE3-p1xB$VsInftq3cXCXUpM(G{MCxo-(GeO`e^O>NDc@l=>-K z8_9|oJ#*%iGFZnh$)*j^^-XgcCxY5b1Yvu7*8K|Ww?dfs&%hrd&XXmP6%Gh7wR3P^lwGA&!IeQP*7i3|Wy7myOl*eU3=0^E zRhRY~%ox~FSRbZPSb*J2IYnvSfXgVcvDEwLhJE|3e6^?@q@PG4Q_Sm?0RO?P33e4h zw)QH?yLTW}@kum6aG)*Li;rIN`B;?`QWQhMuK?)lc7eqWVJqt%GDEOG#O{?~j(i*uJBvZkSI5zDb`Va|MPzQizf@svGA`7|Bl35d)dL{FbhD2Jx!4hZQ_MUuE+P zY7#s9$?;pQWpeFojTMMkiR}HHhiVB&qo3QjR4bt{qJZf1c@qZS^dl~ z@%#%(&)c**<-EMJv??Aq6gn~doius_2kYYj?WiT?j;B$2y{yB(pcs;0M3<|HJs5Hx zy2^yMOL@mM7>{O?$pn3R?kcUQE{W{;CG0vZ05iph$)ametAeHur9@J{J)tT>6_msv# zpIpVwXXa4y+4-_`Ca8@Xklpdz7_L>8*1daffD?cDeN~^`5^0v|pTP(6FDX?l?*GsJ z1+j6J;9SG#$R_$CqK5+l{=arpt*Leqy=3EOY;Eo>?2)Tbr6|YV|rwv5kCA^ zdkb%pS&94@Q7xvXDb~a6!wP#x5vrX03bIQd?E}LoROPJ&IG&!R@D!Q}v}=mT4}%K4bf;6~X;ft2h%T$XrIk?73DemcW)h z8`ixL;!-!n7j?OauZKbA7ixEQHOqc0isYh8qtGtt*{T#h~qbYwR3$eLl zEkp^ySLOr?_0E-jWYE?>VE^$r59|Vo&i_dlma7&O`PM(C;gpeNTW!L#FZV<9S#}?` z^P2h8YRp+42R~LwI52ZS7_nKF6~a;?uD{6Niq8jHHruRn+r@&p#amSMm0axqJhI+;~-tJl>Q`Nw5gAcnbi-IbbypD-Q5ooD(c@l%052vx6Z zJ!JHxeBz-#|36Al*xZn%MIRBL&hI_I*OlMhAx^zW-aQ&~(PbxSc_lT2=^#&oVL}mV zq~G&(`1)TtJ2`~q2391p-jtkWo+PCt{@voxand=&J8ZMYS2$_8317dKvxC$qBAA_&{x7`+isy_Q507V^M;9#ujlRz)TuEx~%z#GcW(<<1X zb=}j!4&mf^Xih|Y9ATyV91dRCCl~&qKu)|xx|_=P*nErR0H)JcnvQB>&?bw(fy&K@ zq+C9Fd5*beRRUcAjmYUA^zaB!h0Rq~X1Ru{A{*r4X!{y#Ck{Q2LySY3Y|*<}9qs~X z-9@`K%xB2-v-WY%oUhtf&(jcIaX->^8{p5oYxml{i5>dpFBj#}sr#O%@_+YwtKMKFe}qEP%%G z-YbAY73>PzeNt}Jz~)>;z$2*}`bzkHV5$-ksIji7`l`JxK%+=qK|6m`$)iTe+~T2p9HX$9YL@~b^lb914)dhw zZ?M9Eb#5t8Lq6u3@iprG>fg}-YuEet2}3zOl3;C%!UGw>K|57PZM3hI_nJkdko`nF z-=EcD{!WZ3^+BPb&msvh1r&9l4^tjd5mDHu7QOpYYqs9NLX{kX8>ScwQI0jvq^PkF zwQj&tDCJx6I}edD+F)n;TOuvKJ;w0hd>% zk%t#zXGSB34p$z_=*!W#taD|+OToq#GE?Ir93DW=HAvPL?|o6)2TeIU!1FML(*_k; z(gf{HV-xuw_t?`?)~HRCL)UuIiM4za?d6#e$QiX0>Hot5#k7e1+31YO7hofa7lrkV zq+-K36Q68_djvFtX+R73pc%T~2)i!%c?Q%5NH;z<}M6Rp;*ax zlW)JsysKG;*|lf!KkDBZwHyv&`TM_nVH};qi{1J zSkxc=;|W&G<<*Z%1nJYc(J8F*^3$J-p$ZpA^bAkXB5f4y&R#F2tC+vd!3v~tgSQaO zm8~1YZ)M$?gRZ64Z*~=IXYzB=PvvkK^18g&eV!GL8-1SfOEtIFv}(zTMIn#lVR zy_C#Suh&_xq{nd)HF%I6$`g6TYyKc?eFQN_cfNPnlvm@gsO|bt=mrwXw@DZ&% zR+@fk z&sf_LZbuW8a~NxB{3>iR4cBu0%Kpy_I63S3vw4@3O0uqDI6wJF~ zP-a#h`^Ro{x?r5{Z$xDmOjb?Og0VCf}$Ttauc)Z$M^Ult5pE^Lh7N zI9Uabijl}_2k_&F2Yogvv$C-UXQPEPC3`N=YddOwmmrpRV}_vTxz;htvh+b;Dry1e zd@SEvEK?%Jb9|DW%09*5ndZuSJGSaw&|!+=L8wD6m@g%{6xqQ z)P$dQr^p7&DLN%uxKsTt)=lK>L`iDRx!j=M?UGDmS%SEPaNt0?(_bV!pfRKmaP?AFKZ&cdq=2=)4X4eOB0OLz zEpB#xVguk9%X=d3h12vIQ=(AI`U}Dw4+;z;5}K^YHuCFCeAjiJ7e7CDD#KgRIeSR^#<>yS|i-O8UUAXh~uOk$Z~7Pcrx9lATu0IGTGVIY5g0=xGP z5E7HviU;q#ZLUvkq=m84g7 zbc~pN1};-v6Gu`#1ChP-_lU<*Nw7KK{L3bbsK3DUHyAkRSNYnu_LOW@N~|?b^y$RV z+dGGm&0@Ecg%r5fM+UE*-Q}J$4qOUx+V57488i0#kSMBBU`R>MG8^ClatANUvTq*3 zPL+-(c*N#0YP?-{eKW_`ewh@{)CtTo0219+D!{;vL+(=~P|+@qLe@%An99a@zCv~EtrB-Yz4 zjC$zG!rk|q<_^@Me;RJ54dCMCarcM~NdT={1oGo$Jb*mGry{tHB7Bc0f zHj;|#$?*HDQ)K~o8P$}fE-b29wwrhMkAN1T? z`<-GT@dx?CbdP!t=+TCadykMK`bFIe*;RM}koPL7_Mbr~YN@(Ld4Nm(d;{GpYnafD zbKDz3JvvZOeW^4fA;p};up!hEjc}jcK|1!ATAbzCNY5U|-CJ0Q~vCPSm{OUeT z_5sM}H!>jjU)$(ePb}!ZDO;5wlA!K!4268@uxwfT>CM9LAyj{$IMUCG_;6*r?* zwpnGBLis29?=i=KP}|S)`T2@)_hqrB@pU=kwQ?b2fUD-*;3GMBUN+uEb?0(b17J211G71z;( F{{ey;n3Mnj literal 0 HcmV?d00001 diff --git a/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs index ee69fa9..597fe89 100644 --- a/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs +++ b/src/CSharp/Rsk.AuthZen.Client.Test/AuthZenClientTests.cs @@ -2560,7 +2560,7 @@ public async Task Evaluation_On404AfterRecheckingMetadata_ShouldThrowAuthZenRequ await sut.Evaluate(evaluationRequest); var act = async () => await sut.Evaluate(evaluationRequest); - act.Should().ThrowAsync(); + await act.Should().ThrowAsync(); calls.Should().Be(5); @@ -2811,8 +2811,8 @@ public async Task BoxcarEvaluation_On404AfterRecheckingMetadata_ShouldThrowAuthZ await sut.Evaluate(evaluationRequest); var act = async () => await sut.Evaluate(evaluationRequest); - - act.Should().ThrowAsync(); + + await act.Should().ThrowAsync(); calls.Should().Be(5); diff --git a/src/CSharp/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj b/src/CSharp/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj index 913837c..327d7fb 100644 --- a/src/CSharp/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj +++ b/src/CSharp/Rsk.AuthZen.Client/Rsk.AuthZen.Client.csproj @@ -3,6 +3,19 @@ netstandard2.0 8.0 + + Rock Solid Knowledge Ltd + AuthZen Client Library + icon.png + https://github.com/RockSolidKnowledge/AuthZenClient + https://github.com/RockSolidKnowledge/AuthZenClient/releases + Copyright 2026 (c) Rock Solid Knowledge Ltd. All rights reserved. + AuthZen + true + true + snupkg + license.txt + false @@ -11,4 +24,9 @@ + + + + + diff --git a/src/CSharp/Rsk.AuthZen.Client/license.txt b/src/CSharp/Rsk.AuthZen.Client/license.txt new file mode 100644 index 0000000..ac62c5c --- /dev/null +++ b/src/CSharp/Rsk.AuthZen.Client/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Rock Solid Knowledge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/Typescript/package.json b/src/Typescript/package.json index ff88f1b..bf0512e 100644 --- a/src/Typescript/package.json +++ b/src/Typescript/package.json @@ -1,7 +1,7 @@ { - "name": "authzen-client", - "version": "1.0.0", - "description": "TypeScript client library for AuthZen Authorization API", + "name": "@rocksolidknowledge/authzen-client", + "version": "0.0.1", + "description": "Client library for AuthZen Authorization API", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { @@ -20,7 +20,7 @@ "security", "typescript" ], - "author": "Your Name", + "author": "Rock Solid Knowledge", "license": "MIT", "devDependencies": { "@babel/core": "^7.28.3", @@ -44,6 +44,11 @@ ], "repository": { "type": "git", - "url": "https://github.com/yourusername/authzen-client.git" - } + "url": "https://github.com/RockSolidKnowledge/AuthZenClient.git" + }, + "type": "module", + "bugs": { + "url": "https://github.com/RockSolidKnowledge/AuthZenClient/issues" + }, + "homepage": "https://github.com/RockSolidKnowledge/AuthZenClient#readme" } diff --git a/src/Typescript/webpack.config.js b/src/Typescript/webpack.config.js index 5076820..7880711 100644 --- a/src/Typescript/webpack.config.js +++ b/src/Typescript/webpack.config.js @@ -1,15 +1,21 @@ -const path = require('path'); +import path from 'path'; +import { fileURLToPath } from 'url'; -module.exports = { +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default { entry: "./src/index.ts", output: { path: path.join(__dirname, "dist"), filename: "index.js", library: { - 'name': 'AuthZenClient', - 'type': 'window' + type: 'module' } }, + experiments: { + outputModule: true + }, module: { rules: [ { From 8344327724d1235bffcf7553ae751e13bec9433b Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 23 Feb 2026 16:06:17 +0000 Subject: [PATCH 24/33] Update tests --- src/Typescript/{jest.config.js => jest.config.cjs} | 0 src/Typescript/package.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Typescript/{jest.config.js => jest.config.cjs} (100%) diff --git a/src/Typescript/jest.config.js b/src/Typescript/jest.config.cjs similarity index 100% rename from src/Typescript/jest.config.js rename to src/Typescript/jest.config.cjs diff --git a/src/Typescript/package.json b/src/Typescript/package.json index bf0512e..ab221d6 100644 --- a/src/Typescript/package.json +++ b/src/Typescript/package.json @@ -7,8 +7,8 @@ "scripts": { "build": "webpack", "build:watch": "webpack --watch", - "test": "jest", - "test:watch": "jest --watch", + "test": "jest --config jest.config.cjs", + "test:watch": "jest --watch --config jest.config.cjs", "lint": "eslint src/**/*.ts", "lint:fix": "eslint src/**/*.ts --fix", "prepublishOnly": "npm run build" From 8c8cb93f1db3d378194439bbbd3c2621300e65ca Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Mon, 23 Feb 2026 16:49:37 +0000 Subject: [PATCH 25/33] Update workflow for trusted publishing --- .github/workflows/typescript.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 5e55654..d13f408 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -10,6 +10,11 @@ on: pull_request: branches: [ "main" ] +# Permissions for npmjs trusted publishing +permissions: + contents: read + id-token: write + jobs: build: From 82253d649b4731439556b8a4d57736356b2ee591 Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 24 Feb 2026 11:35:51 +0000 Subject: [PATCH 26/33] Update readmes to be inline with package contents --- src/CSharp/README.md | 343 +++++++++++++++++++++++++++++++++++++- src/Typescript/README.md | 348 +++++++++++++++++++++++---------------- 2 files changed, 545 insertions(+), 146 deletions(-) diff --git a/src/CSharp/README.md b/src/CSharp/README.md index b1df2fe..8053e7f 100644 --- a/src/CSharp/README.md +++ b/src/CSharp/README.md @@ -1 +1,342 @@ -# AuthZenClient \ No newline at end of file +# AuthZen C# Client + +A C# client library for interacting with [AuthZen](https://openid.github.io/authzen/)-compliant Policy Decision Points (PDPs). This library implements the AuthZen Authorization API 1.0 specification. + +## Features + +- **Discovery** - Automatic PDP configuration via `/.well-known/authzen-configuration` +- **Access Evaluation API** - Single authorization decisions +- **Access Evaluations API** - Batch (boxcar) authorization decisions with multiple evaluation semantics +- **Fluent Builders** - Builder pattern for constructing evaluation requests +- **Property Bags** - Flexible key-value properties on subjects, resources, actions, and context +- **Compatibility** - Targets .NET Standard 2.0 for broad runtime support + +## Installation + +```bash +dotnet add package Rsk.AuthZen.Client +``` + +Or via the NuGet Package Manager: + +``` +Install-Package Rsk.AuthZen.Client +``` + +## Quick Start + +The client uses AuthZen discovery to automatically resolve evaluation endpoints. On the first call to `Evaluate()`, the client fetches `/.well-known/authzen-configuration` from the authorization URL and caches the result. + +### Registration + +Register the client in your dependency injection container: + +```csharp +services.AddHttpClient(); +services.Configure(options => +{ + options.AuthorizationUrl = "https://pdp.mycompany.com"; +}); +services.AddTransient(); +``` + +### Basic Evaluation + +```csharp +// Build a single evaluation request using the fluent builder +var request = new AuthZenSingleRequestBuilder() + .SetSubject("alice@example.com", "user") + .SetAction("can_read") + .SetResource("123", "document") + .Build(); + +// Evaluate +AuthZenResponse response = await authZenClient.Evaluate(request); + +if (response.Decision == Decision.Permit) +{ + Console.WriteLine("Access granted"); +} +else +{ + Console.WriteLine("Access denied"); +} +``` + +## API Reference + +### Client Configuration + +The client is configured via `AuthZenClientOptions` and uses `IHttpClientFactory` for HTTP requests: + +```csharp +public class AuthZenClientOptions +{ + /// + /// Base URL of the AuthZen authorization service (required). + /// + public string AuthorizationUrl { get; set; } +} +``` + +The `AuthZenClient` constructor requires: + +| Parameter | Type | Description | +|---|---|---| +| `httpClientFactory` | `IHttpClientFactory` | Factory for creating `HttpClient` instances | +| `options` | `IOptions` | Configuration options | + +### Discovery + +Fetch the PDP's AuthZen configuration from `/.well-known/authzen-configuration`. This is called automatically before the first evaluation, but you can also call it explicitly: + +```csharp +AuthZenMetadataResponse metadata = await authZenClient.GetMetadata(); + +Console.WriteLine(metadata.PolicyDecisionPoint); +Console.WriteLine(metadata.AccessEvaluationEndpoint); +Console.WriteLine(metadata.AccessEvaluationsEndpoint); +``` + +The returned `AuthZenMetadataResponse` contains: + +| Property | Type | Description | +|---|---|---| +| `PolicyDecisionPoint` | `string` | Base URL of the PDP (required) | +| `AccessEvaluationEndpoint` | `string` | Single evaluation endpoint (required) | +| `AccessEvaluationsEndpoint` | `string` | Batch evaluations endpoint | +| `SearchSubjectEndpoint` | `string` | Subject search endpoint | +| `SearchResourceEndpoint` | `string` | Resource search endpoint | +| `SearchActionEndpoint` | `string` | Action search endpoint | + +### Single Access Evaluation + +Build and evaluate a single authorization request using `AuthZenSingleRequestBuilder`: + +```csharp +var request = new AuthZenSingleRequestBuilder() + .SetCorrelationId("req-12345") + .SetSubject("alice@example.com", "user") + .Add("department", "Sales") + .Add("role", "Manager") + .SetAction("can_read") + .Add("method", "GET") + .SetResource("123", "document") + .Add("classification", "confidential") + .SetContext() + .Add("location", "office") + .Add("time", DateTime.UtcNow.ToString("o")) + .Build(); + +AuthZenResponse response = await authZenClient.Evaluate(request); + +Console.WriteLine($"Decision: {response.Decision}"); // Permit or Deny +Console.WriteLine($"Correlation: {response.CorrelationId}"); +Console.WriteLine($"Context: {response.Context}"); +``` + +The builder methods `SetSubject()`, `SetAction()`, `SetResource()`, and `SetContext()` each return an `IAuthZenPropertyBag`, allowing you to chain `.Add(name, value)` calls to attach additional properties. + +### Response + +The `AuthZenResponse` contains: + +| Property | Type | Description | +|---|---|---| +| `Decision` | `Decision` | `Decision.Permit` or `Decision.Deny` | +| `Context` | `string` | Context information from the PDP response | +| `CorrelationId` | `string` | Request correlation ID from the `X-Request-ID` header | + +### Batch (Boxcar) Access Evaluations + +Evaluate multiple authorization requests in a single call using `AuthZenBoxcarRequestBuilder`: + +```csharp +var request = new AuthZenBoxcarRequestBuilder() + .SetCorrelationId("batch-001") + // Default values applied to any evaluation that omits the field + .SetDefaultSubject("alice@example.com", "user") + .SetDefaultAction("can_read") + // Individual evaluations + .AddRequest() + .SetResource("doc-1", "document") + .AddRequest() + .SetResource("doc-2", "document") + .SetAction("can_write") // Overrides the default action + .AddRequest() + .SetSubject("bob@example.com", "user") // Overrides the default subject + .SetResource("doc-3", "document") + .Build(); + +AuthZenBoxcarResponse response = await authZenClient.Evaluate(request); + +foreach (var evaluation in response.Evaluations) +{ + Console.WriteLine($"Decision: {evaluation.Decision}"); +} +``` + +#### Default Values + +The boxcar builder supports setting default values for subject, resource, action, and context via `SetDefaultSubject()`, `SetDefaultResource()`, `SetDefaultAction()`, and `SetDefaultContext()`. These defaults are applied to any individual evaluation that does not specify that field. Individual evaluations override defaults when both are supplied. + +#### Fallback Behaviour + +If a boxcar request contains no individual evaluations (only defaults), the client automatically falls back to a single evaluation using the default values. + +If the PDP does not advertise a batch evaluations endpoint (`AccessEvaluationsEndpoint` is missing from discovery), the client throws a `NotSupportedException`. + +### Evaluation Semantics + +The batch evaluation API supports three evaluation semantics via `SetEvaluationSemantics()`: + +```csharp +var request = new AuthZenBoxcarRequestBuilder() + .SetDefaultSubject("alice@example.com", "user") + .SetEvaluationSemantics(BoxcarSemantics.DenyOnFirstDeny) + .AddRequest() + .SetAction("can_read") + .SetResource("doc-1", "document") + .AddRequest() + .SetAction("can_read") + .SetResource("doc-2", "document") + .Build(); +``` + +| Semantic | Description | +|---|---| +| `BoxcarSemantics.ExecuteAll` | Execute all evaluations and return all results (default) | +| `BoxcarSemantics.DenyOnFirstDeny` | Stop and return on the first denial (short-circuit AND) | +| `BoxcarSemantics.PermitOnFirstPermit` | Stop and return on the first permit (short-circuit OR) | + +## Error Handling + +The client throws `AuthZenRequestFailureException` when HTTP requests to the PDP fail: + +```csharp +try +{ + var response = await authZenClient.Evaluate(request); +} +catch (AuthZenRequestFailureException ex) +{ + // HTTP error from the PDP (e.g. 400, 401, 500) + Console.Error.WriteLine($"AuthZen request failed: {ex.Message}"); +} +catch (NotSupportedException ex) +{ + // Batch endpoint not supported by this PDP + Console.Error.WriteLine($"Not supported: {ex.Message}"); +} +``` + +The client also implements automatic retry on 404 responses — if an evaluation endpoint returns 404, the client re-fetches the discovery metadata and retries once. + +## Advanced Examples + +### Rich Context Evaluation + +```csharp +var request = new AuthZenSingleRequestBuilder() + .SetSubject("alice@example.com", "user") + .Add("department", "Sales") + .Add("role", "Manager") + .Add("clearance_level", "confidential") + .SetAction("can_read") + .Add("method", "GET") + .Add("api_endpoint", "/documents/123") + .SetResource("123", "document") + .Add("owner", "bob@example.com") + .Add("classification", "confidential") + .Add("project", "Project Alpha") + .SetContext() + .Add("location", "office") + .Add("device_type", "laptop") + .Add("ip_address", "192.168.1.100") + .Add("time", DateTime.UtcNow.ToString("o")) + .Build(); + +AuthZenResponse response = await authZenClient.Evaluate(request); +``` + +### Batch Evaluation with Short-Circuit Logic + +```csharp +var request = new AuthZenBoxcarRequestBuilder() + .SetDefaultSubject("alice@example.com", "user") + .SetEvaluationSemantics(BoxcarSemantics.DenyOnFirstDeny) + .AddRequest() + .SetAction("can_read") + .SetResource("1", "document") + .AddRequest() + .SetAction("can_read") + .SetResource("2", "document") + .AddRequest() + .SetAction("can_read") + .SetResource("3", "document") + .Build(); + +AuthZenBoxcarResponse response = await authZenClient.Evaluate(request); +Console.WriteLine($"Evaluated {response.Evaluations.Count} requests"); +``` + +### Batch Evaluation with Mixed Defaults + +```csharp +var request = new AuthZenBoxcarRequestBuilder() + // Defaults applied to evaluations that omit the field + .SetDefaultSubject("default-user@company.com", "user") + .SetDefaultResource("shared-document", "document") + .SetDefaultAction("read") + .SetDefaultContext() + .Add("environment", "production") + // Override subject only + .AddRequest() + .SetSubject("alice@company.com", "user") + // Override resource and action + .AddRequest() + .SetResource("user-service", "api") + .SetAction("execute") + .Build(); + +AuthZenBoxcarResponse response = await authZenClient.Evaluate(request); +``` + +## Compatibility + +The library targets **.NET Standard 2.0**, which is supported by: + +- .NET Core 2.0+ +- .NET 5+ +- .NET Framework 4.6.1+ + +### Dependencies + +| Package | Version | +|---|---| +| `Microsoft.Extensions.Http` | 9.0.3 | +| `Microsoft.Extensions.Options` | 9.0.3 | +| `System.Text.Json` | 9.0.3 | + +## Development + +### Building + +```bash +dotnet build +``` + +### Testing + +```bash +dotnet test +``` + +## License + +See LICENSE file for details. + +## Related + +- [AuthZen Specification](https://openid.github.io/authzen/) +- [OpenID Foundation](https://openid.net/) \ No newline at end of file diff --git a/src/Typescript/README.md b/src/Typescript/README.md index 3feee9e..9e4b432 100644 --- a/src/Typescript/README.md +++ b/src/Typescript/README.md @@ -1,52 +1,46 @@ # AuthZen TypeScript Client -A comprehensive TypeScript client library for interacting with [AuthZen](https://openid.github.io/authzen/)-compliant Policy Decision Points (PDPs). This library implements the AuthZen Authorization API 1.0 specification. +A TypeScript client library for interacting with [AuthZen](https://openid.github.io/authzen/)-compliant Policy Decision Points (PDPs). This library implements the AuthZen Authorization API 1.0 specification. ## Features -- ✅ **Access Evaluation API** - Single authorization decisions -- ✅ **Access Evaluations API** - Batch authorization decisions with multiple evaluation semantics -- 🔄 **Search APIs** - Subject, Resource, and Action search (coming soon) -- 🛡️ **Type Safety** - Full TypeScript support with comprehensive type definitions -- 🚀 **Modern** - Built with ES2020, supports both Node.js and browser environments -- 🔧 **Flexible** - Configurable fetch implementation, timeouts, and custom headers -- 📝 **Well Documented** - Comprehensive JSDoc comments and examples +- **Discovery** - Automatic PDP configuration via `/.well-known/authzen-configuration` +- **Access Evaluation API** - Single authorization decisions +- **Access Evaluations API** - Batch authorization decisions with multiple evaluation semantics +- **Type Safety** - Full TypeScript support with comprehensive type definitions +- **Validation** - Built-in request validation with descriptive error messages +- **Modern** - Uses global `fetch`; works with Node.js 18+ and modern browsers ## Installation ```bash -npm install authzen-client -``` - -For Node.js environments, you'll also need to install node-fetch: - -```bash -npm install node-fetch -npm install --save-dev @types/node-fetch +npm install @rocksolidknowledge/authzen-client ``` ## Quick Start +The client uses AuthZen discovery to automatically resolve evaluation endpoints. On the first call to `evaluate()` or `evaluations()`, the client fetches `/.well-known/authzen-configuration` from the PDP URL and caches the result. + ```typescript -import { AuthZenClient, createSubject, createAction, createResource } from 'authzen-client'; +import { AuthZenClient } from '@rocksolidknowledge/authzen-client'; -// Create a client +// Create a client pointing at your PDP const client = new AuthZenClient({ - baseUrl: 'https://pdp.mycompany.com', + pdpUrl: 'https://pdp.mycompany.com', token: 'your-bearer-token', // Optional }); -// Simple access evaluation +// Single access evaluation const response = await client.evaluate({ - subject: createSubject('user', 'alice@example.com'), - action: createAction('can_read'), - resource: createResource('document', '123'), + subject: { type: 'user', id: 'alice@example.com' }, + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' }, }); if (response.decision) { - console.log('✅ Access granted'); + console.log('Access granted'); } else { - console.log('❌ Access denied'); + console.log('Access denied'); } ``` @@ -55,16 +49,39 @@ if (response.decision) { ### Client Configuration ```typescript -interface AuthZenClientConfig { - baseUrl: string; // PDP base URL (required) - apiVersion?: string; // API version (default: 'v1') - token?: string; // Bearer token for authentication - headers?: Record; // Custom headers - timeout?: number; // Request timeout in ms (default: 30000) - fetch?: Function; // Custom fetch implementation -} +import { AuthZenClientConfig } from '@rocksolidknowledge/authzen-client'; + +const client = new AuthZenClient({ + pdpUrl: 'https://pdp.mycompany.com', // PDP base URL (required) + token: 'your-bearer-token', // Bearer token for authentication (optional) + headers: { 'X-Custom': 'value' }, // Additional request headers (optional) + timeout: 10000, // Request timeout in ms (default: 10000) +}); +``` + +### Discovery + +Fetch the PDP's AuthZen configuration from `/.well-known/authzen-configuration`. This is called automatically before the first evaluation, but you can also call it explicitly: + +```typescript +const config = await client.discover(); + +console.log(config.policy_decision_point); +console.log(config.access_evaluation_endpoint); +console.log(config.access_evaluations_endpoint); ``` +The returned `AuthZenConfiguration` object may contain: + +| Property | Description | +|---|---| +| `policy_decision_point` | Base URL of the PDP (required) | +| `access_evaluation_endpoint` | Single evaluation endpoint | +| `access_evaluations_endpoint` | Batch evaluations endpoint | +| `search_subject_endpoint` | Subject search endpoint | +| `search_resource_endpoint` | Resource search endpoint | +| `search_action_endpoint` | Action search endpoint | + ### Single Access Evaluation Evaluate a single authorization request: @@ -86,172 +103,182 @@ const response = await client.evaluate({ properties: { classification: 'confidential' } }, context: { - time: '2024-01-01T12:00:00Z', + time: new Date().toISOString(), location: 'office' } }); + +console.log(response.decision); // true or false ``` +For single evaluations, `subject`, `action`, and `resource` are all required. + ### Batch Access Evaluations -Evaluate multiple authorization requests in a single call: +Evaluate multiple authorization requests in a single call using `evaluations()`: ```typescript -const response = await client.evaluateBatch({ +const response = await client.evaluations({ // Default values applied to all evaluations - subject: createSubject('user', 'alice@example.com'), + subject: { type: 'user', id: 'alice@example.com' }, context: { time: new Date().toISOString() }, - - // Individual evaluations + + // Individual evaluations (can omit fields covered by defaults) + // Values in individual evaluations will override a default if both are supplied evaluations: [ { - action: createAction('can_read'), - resource: createResource('document', '123') + action: { name: 'can_read' }, + resource: { type: 'document', id: '123' } }, { - action: createAction('can_write'), - resource: createResource('document', '456') + action: { name: 'can_write' }, + resource: { type: 'document', id: '456' } } ], - + options: { - evaluations_semantic: 'execute_all' // or 'deny_on_first_deny' or 'permit_on_first_permit' + evaluations_semantic: 'execute_all' } }); -``` - -### Evaluation Semantics - -The batch evaluation API supports three evaluation semantics: -- **`execute_all`** (default) - Execute all evaluations and return all results -- **`deny_on_first_deny`** - Stop and return on the first denial (short-circuit AND) -- **`permit_on_first_permit`** - Stop and return on the first permit (short-circuit OR) - -## Utility Functions - -The library provides helpful utility functions for creating AuthZen objects: - -```typescript -import { - createSubject, - createResource, - createAction, - createContext, - createContextWithTime, - SubjectTypes, - ResourceTypes, - ActionNames -} from 'authzen-client'; - -// Create objects with utilities -const user = createSubject(SubjectTypes.USER, 'alice@example.com'); -const document = createResource(ResourceTypes.DOCUMENT, '123'); -const readAction = createAction(ActionNames.READ); -const context = createContextWithTime({ location: 'office' }); +response.evaluations.forEach((result, i) => { + console.log(`Evaluation ${i}: ${result.decision ? 'ALLOW' : 'DENY'}`); +}); ``` -### Built-in Constants - -```typescript -// Subject types -SubjectTypes.USER // 'user' -SubjectTypes.SERVICE // 'service' -SubjectTypes.GROUP // 'group' +### Evaluation Semantics -// Resource types -ResourceTypes.DOCUMENT // 'document' -ResourceTypes.API // 'api' -ResourceTypes.FOLDER // 'folder' +The batch evaluation API supports three evaluation semantics via `options.evaluations_semantic`: -// Action names -ActionNames.READ // 'can_read' -ActionNames.WRITE // 'can_write' -ActionNames.DELETE // 'can_delete' -``` +| Semantic | Description | +|---|---| +| `execute_all` | Execute all evaluations and return all results (default) | +| `deny_on_first_deny` | Stop and return on the first denial (short-circuit AND) | +| `permit_on_first_permit` | Stop and return on the first permit (short-circuit OR) | ## Error Handling -The client throws `AuthZenError` for API-related errors: +The client provides specific error classes for different failure modes: ```typescript -import { AuthZenError } from 'authzen-client'; +import { + AuthZenError, + AuthZenValidationError, + AuthZenRequestError, + AuthZenResponseError, + AuthZenNetworkError, + AuthZenDiscoveryError, +} from '@rocksolidknowledge/authzen-client'; try { const response = await client.evaluate(request); } catch (error) { - if (error instanceof AuthZenError) { - console.error('Status:', error.status); - console.error('Message:', error.message); + if (error instanceof AuthZenValidationError) { + // Request failed local validation (e.g. missing subject) + console.error('Validation:', error.message); + } else if (error instanceof AuthZenDiscoveryError) { + // Discovery endpoint returned invalid configuration + console.error('Discovery:', error.message); + } else if (error instanceof AuthZenRequestError) { + // PDP returned an HTTP error (4xx/5xx) + console.error('HTTP error:', error.statusCode, error.message); + console.error('Response data:', error.responseData); + } else if (error instanceof AuthZenResponseError) { + // Response was not valid JSON + console.error('Response error:', error.statusCode, error.message); + } else if (error instanceof AuthZenNetworkError) { + // Network failure or timeout + console.error('Network:', error.message); + } else if (error instanceof AuthZenError) { + // Base error class — catches all of the above + console.error('AuthZen error:', error.message); console.error('Request ID:', error.requestId); } } ``` -## Node.js Usage - -For Node.js environments, provide a fetch implementation: - -```typescript -import fetch from 'node-fetch'; -import { AuthZenClient } from 'authzen-client'; - -const client = new AuthZenClient({ - baseUrl: 'https://pdp.mycompany.com', - fetch: fetch as any, // Provide fetch implementation -}); -``` +All error classes extend `AuthZenError`, which includes an optional `requestId` for correlation. -## Browser Usage +## Built-in Constants -In browser environments, the global `fetch` API is used automatically: +The library exports commonly used constant values: ```typescript -import { AuthZenClient } from 'authzen-client'; +import { SubjectTypes, ResourceTypes, ActionNames, HttpStatusCodes } from '@rocksolidknowledge/authzen-client'; -const client = new AuthZenClient({ - baseUrl: 'https://pdp.mycompany.com', - // No fetch needed - uses browser's global fetch -}); +// Subject types +SubjectTypes.USER // 'user' +SubjectTypes.SERVICE // 'service' +SubjectTypes.GROUP // 'group' +SubjectTypes.ROLE // 'role' + +// Resource types +ResourceTypes.DOCUMENT // 'document' +ResourceTypes.API // 'api' +ResourceTypes.FOLDER // 'folder' +ResourceTypes.DATABASE // 'database' +ResourceTypes.SERVICE // 'service' + +// Action names +ActionNames.READ // 'can_read' +ActionNames.WRITE // 'can_write' +ActionNames.DELETE // 'can_delete' +ActionNames.EXECUTE // 'can_execute' +ActionNames.CREATE // 'can_create' +ActionNames.UPDATE // 'can_update' +ActionNames.VIEW // 'can_view' +ActionNames.EDIT // 'can_edit' ``` ## Advanced Examples -### Complex Authorization with Rich Context +### Rich Context Evaluation ```typescript const response = await client.evaluate({ - subject: createSubject('user', 'alice@example.com', { - department: 'Sales', - role: 'Manager', - clearance_level: 'confidential' - }), - action: createAction('can_read', { - method: 'GET', - api_endpoint: '/documents/123' - }), - resource: createResource('document', '123', { - owner: 'bob@example.com', - classification: 'confidential', - project: 'Project Alpha' - }), - context: createContextWithTime({ + subject: { + type: 'user', + id: 'alice@example.com', + properties: { + department: 'Sales', + role: 'Manager', + clearance_level: 'confidential' + } + }, + action: { + name: 'can_read', + properties: { + method: 'GET', + api_endpoint: '/documents/123' + } + }, + resource: { + type: 'document', + id: '123', + properties: { + owner: 'bob@example.com', + classification: 'confidential', + project: 'Project Alpha' + } + }, + context: { location: 'office', device_type: 'laptop', - ip_address: '192.168.1.100' - }) + ip_address: '192.168.1.100', + time: new Date().toISOString() + } }); ``` ### Batch Evaluation with Short-Circuit Logic ```typescript -const response = await client.evaluateBatch({ - subject: createSubject('user', 'alice@example.com'), +const response = await client.evaluations({ + subject: { type: 'user', id: 'alice@example.com' }, evaluations: [ - { action: createAction('can_read'), resource: createResource('document', '1') }, - { action: createAction('can_read'), resource: createResource('document', '2') }, - { action: createAction('can_read'), resource: createResource('document', '3') } + { action: { name: 'can_read' }, resource: { type: 'document', id: '1' } }, + { action: { name: 'can_read' }, resource: { type: 'document', id: '2' } }, + { action: { name: 'can_read' }, resource: { type: 'document', id: '3' } } ], options: { evaluations_semantic: 'deny_on_first_deny' // Stop on first denial @@ -261,6 +288,41 @@ const response = await client.evaluateBatch({ console.log(`Evaluated ${response.evaluations.length} requests`); ``` +### Batch Evaluation with Mixed Defaults + +Individual evaluations can omit fields that are provided as top-level defaults: + +```typescript +const response = await client.evaluations({ + // These defaults apply to any evaluation that omits the field + subject: { type: 'user', id: 'default-user@company.com' }, + resource: { type: 'document', id: 'shared-document' }, + action: { name: 'read' }, + context: { environment: 'production' }, + + evaluations: [ + // Uses all defaults + {}, + // Overrides subject only + { subject: { type: 'user', id: 'alice@company.com' } }, + // Overrides resource and action + { + resource: { type: 'api', id: 'user-service' }, + action: { name: 'execute' } + }, + ] +}); +``` + +## Compatibility + +The client uses the global `fetch` API, which is available natively in: + +- **Node.js** 18+ (built-in global `fetch`) +- **Modern browsers** (Chrome, Firefox, Safari, Edge) + +No additional fetch polyfill is required for these environments. + ## Development ### Building @@ -281,10 +343,6 @@ npm test npm run lint ``` -## Contributing - -Contributions are welcome! Please read our contributing guidelines and submit pull requests for any improvements. - ## License MIT License - see LICENSE file for details. @@ -300,8 +358,8 @@ MIT License - see LICENSE file for details. ### 1.0.0 - Initial release +- Support endpoint discovery - Support for Access Evaluation API - Support for Access Evaluations API (batch) - TypeScript support -- Utility functions -- Comprehensive documentation +- Documentation From 4e3b54785a40594fd3c90fd5d4a6f277312b459b Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 24 Feb 2026 12:01:32 +0000 Subject: [PATCH 27/33] Typescript workflow update to use trusted publish Retry Retry --- .github/workflows/typescript.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index d13f408..ddfa140 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -72,9 +72,6 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - registry-url: 'https://registry.npmjs.org' + node-version: '22' - name: Publish to npm - run: npm publish ./package/*.tgz --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish ./package/*.tgz --access public --provenance From 44ba40d85ba4d450deca42600dde46b13cb1bdec Mon Sep 17 00:00:00 2001 From: Patrick Allwood Date: Tue, 24 Feb 2026 13:19:55 +0000 Subject: [PATCH 28/33] Retry --- .github/workflows/typescript.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index ddfa140..fcc2cb9 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -72,6 +72,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' + registry-url: 'https://registry.npmjs.org' - name: Publish to npm run: npm publish ./package/*.tgz --access public --provenance From 97a87861aaba9cd9fe57b0035bbdde81ce3ed38f Mon Sep 17 00:00:00 2001 From: andrewclymer Date: Fri, 10 Apr 2026 14:33:28 +0100 Subject: [PATCH 29/33] AuthZen Example using Enforcer --- .../.idea/.gitignore | 15 ++ .../.idea/copilot.data.migration.agent.xml | 6 + .../.idea/copilot.data.migration.ask.xml | 6 + .../copilot.data.migration.ask2agent.xml | 6 + .../.idea/copilot.data.migration.edit.xml | 6 + .../.idea/encodings.xml | 4 + .../.idea/indexLayout.xml | 8 + .../.idea.PolicyDrivenExpenses/.idea/vcs.xml | 7 + .../AuthZenPolicyServer.csproj | 24 +++ .../AuthZenPolicyServer/Pages/Home.cshtml | 27 ++++ .../AuthZenPolicyServer/Pages/Home.cshtml.cs | 59 +++++++ .../Pages/_ViewImports.cshtml | 2 + .../Policies/expenses.alfa | 102 ++++++++++++ .../AuthZenPolicyServer/Policies/global.alfa | 9 ++ .../AuthZenPolicyServer/Program.cs | 39 +++++ .../Properties/launchSettings.json | 23 +++ .../SubjectAttributeProvider.cs | 33 ++++ .../appsettings.Development.json | 8 + .../AuthZenPolicyServer/appsettings.json | 9 ++ .../PolicyDrivenExpenses.sln | 22 +++ .../PolicyDrivenExpenses.sln.DotSettings.user | 16 ++ .../WebApp/ApplicationDbContext.cs | 11 ++ .../AllowAllAuthorizeExpenseClaimActions.cs | 34 ++++ .../AuthZenAuthorizeExpenseClaimActions.cs | 108 +++++++++++++ .../WebApp/Authorization/AuthorizeResult.cs | 14 ++ .../DenyAllAuthorizeExpenseClaimActions.cs | 35 +++++ .../IAuthorizeExpenseClaimActions.cs | 13 ++ .../WebApp/Domain/IExpenseClaimService.cs | 22 +++ .../WebApp/Domain/IExpenseClaimSubmission.cs | 21 +++ .../Domain/IGenerateAccessDeniedContent.cs | 33 ++++ .../WebApp/Domain/IManagerLookupService.cs | 6 + .../Domain/InMemoryExpenseClaimService.cs | 147 +++++++++++++++++ .../Domain/InMemoryManagerLookupService.cs | 40 +++++ .../WebApp/Pages/AccessDenied.cshtml | 24 +++ .../WebApp/Pages/AccessDenied.cshtml.cs | 17 ++ .../WebApp/Pages/Account/Login.cshtml | 47 ++++++ .../WebApp/Pages/Account/Login.cshtml.cs | 59 +++++++ .../WebApp/Pages/Account/Logout.cshtml | 19 +++ .../WebApp/Pages/Account/Logout.cshtml.cs | 16 ++ .../WebApp/Pages/Expenses/Approve.cshtml | 72 +++++++++ .../WebApp/Pages/Expenses/Approve.cshtml.cs | 122 +++++++++++++++ .../WebApp/Pages/Expenses/Create.cshtml | 108 +++++++++++++ .../WebApp/Pages/Expenses/Create.cshtml.cs | 148 ++++++++++++++++++ .../WebApp/Pages/Home.cshtml | 45 ++++++ .../WebApp/Pages/Home.cshtml.cs | 10 ++ .../WebApp/Pages/Shared/_Layout.cshtml | 84 ++++++++++ .../Shared/_ValidationScriptsPartial.cshtml | 4 + .../WebApp/Pages/_Layout.cshtml | 2 + .../WebApp/Pages/_ViewImports.cshtml | 4 + .../WebApp/Pages/_ViewStart.cshtml | 3 + .../PolicyDrivenExpenses/WebApp/Program.cs | 102 ++++++++++++ .../WebApp/Properties/launchSettings.json | 23 +++ .../PolicyDrivenExpenses/WebApp/README.md | 23 +++ .../PolicyDrivenExpenses/WebApp/WebApp.csproj | 15 ++ .../WebApp/appsettings.Development.json | 8 + .../WebApp/appsettings.json | 9 ++ 56 files changed, 1879 insertions(+) create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/_ViewImports.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/expenses.alfa create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/global.alfa create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Properties/launchSettings.json create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.Development.json create mode 100644 Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.json create mode 100644 Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln create mode 100644 Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/ApplicationDbContext.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AllowAllAuthorizeExpenseClaimActions.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthorizeResult.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/DenyAllAuthorizeExpenseClaimActions.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/IAuthorizeExpenseClaimActions.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimService.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimSubmission.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IGenerateAccessDeniedContent.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IManagerLookupService.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryExpenseClaimService.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryManagerLookupService.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_Layout.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_ValidationScriptsPartial.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_Layout.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewImports.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewStart.cshtml create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Program.cs create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/Properties/launchSettings.json create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/WebApp.csproj create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.Development.json create mode 100644 Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.json diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore new file mode 100644 index 0000000..a3db6dc --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/.idea.AuthZenExample.iml +/modules.xml +/projectSettingsUpdater.xml +/contentModel.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml new file mode 100644 index 0000000..ba43f69 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj new file mode 100644 index 0000000..92cfa97 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj @@ -0,0 +1,24 @@ + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + ..\..\..\..\..\usr\local\share\dotnet\shared\Microsoft.AspNetCore.App\10.0.3\Microsoft.AspNetCore.dll + + + diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml new file mode 100644 index 0000000..812f566 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml @@ -0,0 +1,27 @@ +@page +@model AuthZenPolicyServer.Pages.HomeModel +@{ + ViewData["Title"] = "Home"; +} + + + +
+

Welcome to the AuthZen Policy Server

+

This server is responsible for serving and managing the expense claim authorization decisions.

+ @foreach (var policy in Model.PolicyFiles) + { +

Policy: @policy.Name

+
+
+
@Html.Raw(Model.PolicyHtml(policy.Content))
+
+
+ } +
diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml.cs b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml.cs new file mode 100644 index 0000000..4d1b285 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/Home.cshtml.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Text.RegularExpressions; +using System.Reflection; + +namespace AuthZenPolicyServer.Pages +{ + public class HomeModel : PageModel + { + public string PolicyText { get; set; } = string.Empty; + public List<(string Name, string Content)> PolicyFiles { get; set; } = new(); + + public void OnGet() + { + var assembly = Assembly.GetExecutingAssembly(); + var resources = assembly.GetManifestResourceNames() + .Where(r => r.Contains(".Policies.") && r.EndsWith(".alfa")); + foreach (var resource in resources) + { + using var stream = assembly.GetManifestResourceStream(resource); + using var reader = new StreamReader(stream!); + var content = reader.ReadToEnd(); + var name = resource.Substring(resource.LastIndexOf(".Policies.") + 10); + PolicyFiles.Add((name, content)); + } + } + + public static string ColorizePolicy(string policy) + { + // Colorize comments first + string result = Regex.Replace(policy, "//.*", "$0", RegexOptions.None, TimeSpan.FromSeconds(1)); + // Colorize strings + result = Regex.Replace(result, "\"([^\"]*)\"", "\"$1\"", RegexOptions.None, TimeSpan.FromSeconds(1)); + // Keywords + string[] keywords = new[] { "namespace", "import", "attribute", "policyset", "policy", "apply", "firstApplicable", "denyUnlessPermit", "permitUnlessDeny", "target", "clause", "rule", "condition", "permit", "deny", "on", "advice" , "money" , "int" , "double" , "time" , "obligation" , "string" , "date" , "let"}; + foreach (var keyword in keywords) + { + result = Regex.Replace(result, $@"\b{keyword}\b", $"{keyword}", RegexOptions.None, TimeSpan.FromSeconds(1)); + } + // Numbers + result = Regex.Replace(result, "(?<=\\s|^)([0-9]+(\\.[0-9]+)?)(?=\\s|$)", "$1", RegexOptions.None, TimeSpan.FromSeconds(1)); + // Braces + result = result.Replace("{", "{"); + result = result.Replace("}", "}"); + // HTML encode everything except our tags + result = Regex.Replace(result, "(<[^>]+>|[^<]+)", match => + { + if (match.Value.StartsWith("<")) + return match.Value; // leave tags alone + return System.Net.WebUtility.HtmlEncode(match.Value); + }); + // Fix double-encoding of quotes inside attributes + result = result.Replace("'", "'").Replace(""", "\""); + return result; + } + + public HtmlString PolicyHtml(string policy) => new HtmlString(ColorizePolicy(policy)); + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/_ViewImports.cshtml b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..e631058 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Pages/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@namespace AuthZenPolicyServer.Pages + diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/expenses.alfa b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/expenses.alfa new file mode 100644 index 0000000..8dc8ceb --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/expenses.alfa @@ -0,0 +1,102 @@ +namespace acmeCorp +{ + + import Oasis.Functions.* + import Oasis.Attributes.* + import Enforcer.AuthZen.* + + attribute ExpenseTotal { id ="total" type=money category=resourceCat} + attribute ApproverId { id ="approver" type=string category=resourceCat} + + // + // Policies for handling the creation, and approval of expense claims + // + policyset expenses + { + target clause ResourceType == "expenses" + apply denyUnlessPermit + + policy CreateExpenseClaim + policy SubmitExpenseClaim + policy ViewClaimsToApprove + policy ApproveAndRejectClaims + } + + policy CreateExpenseClaim { + target clause Action == "CreateClaim" + apply denyUnlessPermit + + rule CanCreateExpenseClaim { + condition Role == "employee" + permit + } + + on deny + { + advice authZenContext + { + error = "Must be an employee to create a claim" + } + } + } + + policy SubmitExpenseClaim + { + apply permitUnlessDeny + + target clause Action == "SubmitClaim" + rule { + condition ExpenseTotal > 1000:money and Role == "employee" + deny + on deny + { + advice authZenContext + { + error = "Claim must be less than 1000 GBP" + } + } + } + rule { + condition Role != "employee" + deny + + on deny + { + advice authZenContext + { + error = "Must be an employee to submit a claim" + } + } + } + } + + policy ViewClaimsToApprove + { + target clause Action == "ListClaimsToApprove" + apply denyUnlessPermit + + rule CanListClaims { + condition Role == "manager" + permit + } + + on deny + { + advice authZenContext + { + error = "Must be a manager to approve claims" + } + } + } + + policy ApproveAndRejectClaims + { + target clause Action == "AcceptClaim" or Action == "RejectClaim" + apply permitUnlessDeny + + rule { + condition Subject.Identifier != ApproverId and Role == "manager" + deny + } + } +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/global.alfa b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/global.alfa new file mode 100644 index 0000000..e5b6306 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Policies/global.alfa @@ -0,0 +1,9 @@ +namespace acmeCorp +{ + policyset global + { + apply firstApplicable + policy expenses + } +} + diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs new file mode 100644 index 0000000..2763256 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs @@ -0,0 +1,39 @@ +using AuthZenPolicyServer; +using Rsk.Enforcer; +using Rsk.Enforcer.AuthZen; +using Rsk.Enforcer.PAP.Store; +using Rsk.Enforcer.PEP; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddRazorPages(); + builder.Services + .AddEnforcer("acmeCorp.global",options => + { + options.Licensee = "DEMO"; + options.LicenseKey = "eyJhdXRoIjoiREVNTyIsImV4cCI6IjIwMjYtMDQtMzBUMDA6MDA6MDAiLCJpYXQiOiIyMDI2LTAzLTMwVDExOjUwOjA1LjQxMDU5NTFaIiwib3JnIjoiREVNTyIsImF1ZCI6N30=.mtYt37KGtQFJ5je0XJGckWrOx6lqCF5QwraMPJyGFgzYOq8sAFARoIjCKpJ0JpbpCRbCcaTFFhekfHU6NLvJka/ZzfsOYM4JHBSQpol2Z38PwkR4p8J6ONBi/SYOIvXrTk48Tf09Tvo2WHeoiZ9/MLu4IN7+w8sib0fUdkt/cY1PKHHzofBBHPsOT4/LOyxZoVIFLsINC5IOCkGf1vkmCADVFTszOY5nwUf3CNBs+C6UwfpHnvggnMnZpanW45WoWDDcQHgxwS13LgH6k+0XBUrPdcFhTR9mlSuboDspctvVeNASUBWcSLLdGY7GhK2RAWEAf9bbsTrSHErqIK+gx0XcDaq+n94q/qW3swJGGjUlcj+PaGPhmoEojYfwFWWZU6y4dz45XC941GpsYZEGYSVos5+oJMdreCOZqoPXhjEiqmRDgNT7llQ4bixr9voW3N1WKrfy6Ftr2ZYPv/tSOZb3wofGkpLSpPAw/XiyWUOkIiuVajR9CM8//pWQCOZodL1/xuXlioW8EVECXoGDhreDaGhc5BIEycJC/Fv0rgrnFxrPbStm8z+jmigGhN7G7quXaZr+VHhr+WEgjqbB3MSUhR1f/jwjKtiQMEoU7EDiC9BsNkV+KmGKr+o23HlvM2mwE5/rOa/ORgJ3LZmad2yBi6CYge8lwmSLWABMEmc="; + }) + .AddPolicyEnforcementPoint(o => o.Bias = PepBias.Deny) + .AddAuthZen() + .AddAuthZenAdvice() + .AddPolicyAttributeProvider() + .AddEmbeddedPolicyStore(typeof(Program).Assembly, "AuthZenPolicyServer.Policies"); + + var app = builder.Build(); + + app.UseEnforcerAuthZen(); + app.UseStaticFiles(); + app.UseRouting(); + app.MapRazorPages(); + app.MapGet("/", context => + { + context.Response.Redirect("/Home"); + return Task.CompletedTask; + }); + app.Run(); + } +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Properties/launchSettings.json b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Properties/launchSettings.json new file mode 100644 index 0000000..5b1460b --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5208", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7064;http://localhost:5208", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs new file mode 100644 index 0000000..4404532 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs @@ -0,0 +1,33 @@ +using Rsk.Enforcer.Oasis.PolicyModel; +using Rsk.Enforcer.PIP; +using Rsk.Enforcer.PolicyModels; + +namespace AuthZenPolicyServer; + +public class AcmeCorpPerson() +{ + [PolicyAttributeValue(PolicyAttributeCategories.Subject, "role")] + public IEnumerable Roles { get; init; } = []; +} + +public class SubjectAttributeProvider : RecordAttributeValueProvider +{ + private static readonly Dictionary people = new() + { + ["bob"] = new AcmeCorpPerson() { Roles = ["employee"]}, + ["alice"] = new AcmeCorpPerson() { Roles = ["employee","manager"]}, + }; + + protected override async Task GetRecordValue(IAttributeResolver attributeResolver, CancellationToken ct) + { + IReadOnlyCollection? identifiers = await attributeResolver + .Resolve(Rsk.Enforcer.Oasis.Attributes.Subject.Identifier, ct); + + string? identifier = identifiers.SingleOrDefault(); + if (identifier == null) return null!; + + AcmeCorpPerson person = new AcmeCorpPerson(); + + return people[identifier]; + } +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.Development.json b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.json b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln b/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln new file mode 100644 index 0000000..62f1199 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp", "WebApp\WebApp.csproj", "{10B37E26-0292-47E2-AFF8-37C074922F77}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthZenPolicyServer", "AuthZenPolicyServer\AuthZenPolicyServer.csproj", "{EF708F15-E567-4151-8BE4-A1FB5BD56EEA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {10B37E26-0292-47E2-AFF8-37C074922F77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10B37E26-0292-47E2-AFF8-37C074922F77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10B37E26-0292-47E2-AFF8-37C074922F77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10B37E26-0292-47E2-AFF8-37C074922F77}.Release|Any CPU.Build.0 = Release|Any CPU + {EF708F15-E567-4151-8BE4-A1FB5BD56EEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF708F15-E567-4151-8BE4-A1FB5BD56EEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF708F15-E567-4151-8BE4-A1FB5BD56EEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF708F15-E567-4151-8BE4-A1FB5BD56EEA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user b/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user new file mode 100644 index 0000000..0f4cf32 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user @@ -0,0 +1,16 @@ + + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/ApplicationDbContext.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/ApplicationDbContext.cs new file mode 100644 index 0000000..746f591 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/ApplicationDbContext.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace WebApp; + +public sealed class ApplicationDbContext(DbContextOptions options) + : IdentityDbContext(options) +{ +} + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AllowAllAuthorizeExpenseClaimActions.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AllowAllAuthorizeExpenseClaimActions.cs new file mode 100644 index 0000000..b7cfbbe --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AllowAllAuthorizeExpenseClaimActions.cs @@ -0,0 +1,34 @@ +using WebApp.Domain; + +namespace WebApp.Authorization; + +public sealed class AllowAllAuthorizeExpenseClaimActions : IAuthorizeExpenseClaimActions +{ + private static readonly AuthorizeResult Success = new AuthorizeResult(true); + + public Task CanCreateClaim(string submitterUserId) + { + return Task.FromResult(Success); + } + + public Task CanSubmitClaim(string submitterUserId, decimal grossCost) + { + return Task.FromResult(Success); + } + + public Task CanApproveAndRejectClaims(string approverUserId) + { + return Task.FromResult(Success); + } + + public Task CanApproveClaims(string approverUserId, IEnumerable submission) + { + return Task.FromResult(Success); + } + + public Task CanRejectClaims(string approverUserId, IEnumerable submission) + { + return Task.FromResult(Success); + } +} + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs new file mode 100644 index 0000000..5182c1e --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs @@ -0,0 +1,108 @@ +using System.Text.Json; +using Rsk.AuthZen.Client; +using WebApp.Domain; + +namespace WebApp.Authorization; + +public class AuthZenAuthorizeExpenseClaimActions(IAuthZenClient client) : IAuthorizeExpenseClaimActions +{ + public Task CanCreateClaim(string submitterUserId) + { + AuthZenSingleRequestBuilder requestBuilder = new AuthZenSingleRequestBuilder(); + + requestBuilder + .SetSubject(submitterUserId, "user"); + + requestBuilder.SetAction("CreateClaim"); + requestBuilder.SetResource("newExpense", "expenses"); + + var request = requestBuilder.Build(); + + return Authorize(request); + } + + + public Task CanSubmitClaim(string submitterUserId, decimal grossCost) + { + AuthZenSingleRequestBuilder requestBuilder = new AuthZenSingleRequestBuilder(); + + requestBuilder + .SetSubject(submitterUserId, "user"); + + requestBuilder.SetAction("SubmitClaim"); + requestBuilder + .SetResource("newExpense", "expenses") + .Add("total",grossCost); + + var request = requestBuilder.Build(); + + return Authorize(request); + } + + public Task CanApproveAndRejectClaims(string approverUserId) + { + AuthZenSingleRequestBuilder requestBuilder = new AuthZenSingleRequestBuilder(); + + requestBuilder + .SetSubject(approverUserId, "user"); + + requestBuilder + .SetAction("ListClaimsToApprove"); + + requestBuilder + .SetResource("expenseList", "expenses") + ; + + var request = requestBuilder.Build(); + + return Authorize(request); + } + + public Task CanApproveClaims(string approverUserId, IEnumerable submissions) + { + return AuthorizeClaimSubmissionsAction(approverUserId, submissions,"AcceptClaim"); + } + + public Task CanRejectClaims(string approverUserId, IEnumerable submissions) + { + return AuthorizeClaimSubmissionsAction(approverUserId, submissions,"RejectClaim"); + } + + private async Task AuthorizeClaimSubmissionsAction(string approverUserId, + IEnumerable submissions , string action) + { + var boxCarRequestBuilder = new AuthZenBoxcarRequestBuilder(); + + foreach (IExpenseClaimSubmission submission in submissions) + { + var rb = boxCarRequestBuilder.AddRequest(); + rb.SetAction(action); + rb.SetSubject(approverUserId, "user"); + rb.SetResource(submission.SubmissionId, "expenses") + .Add("approver", submission.ApproverUserId); + } + + AuthZenBoxcarEvaluationRequest? request = boxCarRequestBuilder.Build(); + + AuthZenBoxcarResponse? result = await client.Evaluate(request); + + bool success = result.Evaluations.All(e => e.Decision == Decision.Permit); + return new AuthorizeResult(success); + } + + private async Task Authorize(AuthZenEvaluationRequest request) + { + AuthZenResponse response = await client.Evaluate(request); + bool success = response.Decision == Decision.Permit; + + if (success == false) + { + string? error = + JsonSerializer.Deserialize(response.Context).GetProperty("error").GetString(); + + return new AuthorizeResult(false, [error ?? String.Empty]); + } + + return new AuthorizeResult(success); + } +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthorizeResult.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthorizeResult.cs new file mode 100644 index 0000000..073a735 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthorizeResult.cs @@ -0,0 +1,14 @@ +namespace WebApp.Authorization; + +public struct AuthorizeResult(bool success, IEnumerable messages) +{ + public IEnumerable Messages { get; } = messages; + private static readonly IEnumerable EmptyMessages = []; + + public AuthorizeResult(bool success) : this(success,EmptyMessages) + { + + } + + public bool Success { get; } = success; +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/DenyAllAuthorizeExpenseClaimActions.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/DenyAllAuthorizeExpenseClaimActions.cs new file mode 100644 index 0000000..83fa72a --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/DenyAllAuthorizeExpenseClaimActions.cs @@ -0,0 +1,35 @@ + using WebApp.Domain; + + namespace WebApp.Authorization; + +public sealed class DenyAllAuthorizeExpenseClaimActions : IAuthorizeExpenseClaimActions +{ + private static readonly AuthorizeResult Failure = new AuthorizeResult(false, ["All requests will be denied"]); + + public Task CanCreateClaim(string submitterUserId) + { + return Task.FromResult(Failure); + } + + public Task CanSubmitClaim(string submitterUserId, decimal grossCost) + { + return Task.FromResult(Failure); + } + + public Task CanApproveAndRejectClaims(string approverUserId) + { + return Task.FromResult(Failure); + } + + public Task CanApproveClaims(string approverUserId, IEnumerable submission) + { + return Task.FromResult(Failure); + } + + public Task CanRejectClaims(string approverUserId, IEnumerable submission) + { + return Task.FromResult(Failure); + } + +} + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/IAuthorizeExpenseClaimActions.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/IAuthorizeExpenseClaimActions.cs new file mode 100644 index 0000000..81fd6a1 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/IAuthorizeExpenseClaimActions.cs @@ -0,0 +1,13 @@ +using WebApp.Domain; + +namespace WebApp.Authorization; + +public interface IAuthorizeExpenseClaimActions +{ + Task CanCreateClaim(string submitterUserId); + Task CanSubmitClaim(string submitterUserId, decimal grossCost); + + Task CanApproveAndRejectClaims(string approverUserId); + Task CanApproveClaims(string approverUserId , IEnumerable submission); + Task CanRejectClaims(string approverUserId , IEnumerable submission); +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimService.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimService.cs new file mode 100644 index 0000000..5b091e2 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimService.cs @@ -0,0 +1,22 @@ +namespace WebApp.Domain; + +public interface IExpenseClaimService +{ + Task Create( + string submitterUserId, + string description, + DateOnly claimDate, + decimal grossCost, + decimal tax, + string costCentre); + + Task GetById(string submissionId); + + Task> GetBySubmitterUserId(string submitterUserId); + + Task> GetByApproverUserId(string approverUserId); + + Task UpdateStatus( + string submissionId, + ExpenseClaimStatus status); +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimSubmission.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimSubmission.cs new file mode 100644 index 0000000..18e8d2b --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IExpenseClaimSubmission.cs @@ -0,0 +1,21 @@ +namespace WebApp.Domain; + +public enum ExpenseClaimStatus +{ + Submitted, + Rejected, + Approved +} + +public interface IExpenseClaimSubmission +{ + string SubmissionId { get; } + string SubmitterUserId { get; } + string ApproverUserId { get; } + string Description { get; } + DateOnly ClaimDate { get; } + decimal GrossCost { get; } + decimal Tax { get; } + string CostCentre { get; } + ExpenseClaimStatus Status { get; } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IGenerateAccessDeniedContent.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IGenerateAccessDeniedContent.cs new file mode 100644 index 0000000..0a3b097 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IGenerateAccessDeniedContent.cs @@ -0,0 +1,33 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Mvc; + +namespace WebApp.Domain; + +public interface IGenerateAccessDeniedContent +{ + ActionResult Redirect(IEnumerable messages); + IEnumerable Messages(string reasonCode); +} + + + +public class InMemoryGeneratedAccessDeniedContent : IGenerateAccessDeniedContent +{ + private ConcurrentDictionary> messagesMap = new(); + + public ActionResult Redirect(IEnumerable messages) + { + string reasonCode = Guid.NewGuid().ToString(); + messagesMap.TryAdd(reasonCode, messages); + return new RedirectToPageResult("/AccessDenied", null , new + { + reason = reasonCode + }, null); + } + + public IEnumerable Messages(string reasonCode) + { + messagesMap.TryRemove(reasonCode, out IEnumerable? messages); + return messages ?? []; + } +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IManagerLookupService.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IManagerLookupService.cs new file mode 100644 index 0000000..2d95644 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/IManagerLookupService.cs @@ -0,0 +1,6 @@ +namespace WebApp.Domain; + +public interface IManagerLookupService +{ + Task GetManagerUserId(string submitterUserId); +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryExpenseClaimService.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryExpenseClaimService.cs new file mode 100644 index 0000000..ccc6f27 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryExpenseClaimService.cs @@ -0,0 +1,147 @@ +using System.Collections.Concurrent; + +namespace WebApp.Domain; + +public sealed class InMemoryExpenseClaimService(IManagerLookupService managerLookupService) : IExpenseClaimService +{ + private readonly IManagerLookupService managerLookupService = managerLookupService; + private static readonly ConcurrentDictionary claims = new(); + + public async Task Create( + string submitterUserId, + string description, + DateOnly claimDate, + decimal grossCost, + decimal tax, + string costCentre) + { + ValidateCreateInput(submitterUserId, description, grossCost, tax, costCentre); + + var approverUserId = await managerLookupService.GetManagerUserId(submitterUserId); + if (string.IsNullOrWhiteSpace(approverUserId)) + { + throw new InvalidOperationException($"No approver found for submitter '{submitterUserId}'."); + } + + var claim = new ExpenseClaimSubmission( + SubmissionId: Guid.NewGuid().ToString("N"), + SubmitterUserId: submitterUserId.Trim(), + ApproverUserId: approverUserId.Trim(), + Description: description.Trim(), + ClaimDate: claimDate, + GrossCost: grossCost, + Tax: tax, + CostCentre: costCentre.Trim(), + Status: ExpenseClaimStatus.Submitted); + + claims[claim.SubmissionId] = claim; + return claim; + } + + public Task GetById(string submissionId) + { + if (string.IsNullOrWhiteSpace(submissionId)) + { + return Task.FromResult(null); + } + + claims.TryGetValue(submissionId, out var claim); + return Task.FromResult(claim); + } + + public Task> GetBySubmitterUserId(string submitterUserId) + { + if (string.IsNullOrWhiteSpace(submitterUserId)) + { + return Task.FromResult>([]); + } + + var normalized = submitterUserId.Trim(); + var results = claims.Values + .Where(c => string.Equals(c.SubmitterUserId, normalized, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(c => c.ClaimDate) + .Cast() + .ToList(); + + return Task.FromResult>(results); + } + + public Task> GetByApproverUserId(string approverUserId) + { + if (string.IsNullOrWhiteSpace(approverUserId)) + { + return Task.FromResult>([]); + } + + var normalized = approverUserId.Trim(); + var results = claims.Values + .Where(c => string.Equals(c.ApproverUserId, normalized, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(c => c.ClaimDate) + .Cast() + .ToList(); + + return Task.FromResult>(results); + } + + public Task UpdateStatus(string submissionId, ExpenseClaimStatus status) + { + if (string.IsNullOrWhiteSpace(submissionId)) + { + throw new ArgumentException("Submission id is required.", nameof(submissionId)); + } + + if (!claims.TryGetValue(submissionId, out var existing)) + { + throw new KeyNotFoundException($"No expense claim found with id '{submissionId}'."); + } + + var updated = existing with { Status = status }; + claims[submissionId] = updated; + + return Task.FromResult(updated); + } + + private static void ValidateCreateInput( + string submitterUserId, + string description, + decimal grossCost, + decimal tax, + string costCentre) + { + if (string.IsNullOrWhiteSpace(submitterUserId)) + { + throw new ArgumentException("Submitter user id is required.", nameof(submitterUserId)); + } + + if (string.IsNullOrWhiteSpace(description)) + { + throw new ArgumentException("Description is required.", nameof(description)); + } + + if (grossCost <= 0) + { + throw new ArgumentOutOfRangeException(nameof(grossCost), "Gross cost must be greater than zero."); + } + + if (tax < 0) + { + throw new ArgumentOutOfRangeException(nameof(tax), "Tax cannot be negative."); + } + + if (string.IsNullOrWhiteSpace(costCentre)) + { + throw new ArgumentException("Cost centre is required.", nameof(costCentre)); + } + } + + private sealed record ExpenseClaimSubmission( + string SubmissionId, + string SubmitterUserId, + string ApproverUserId, + string Description, + DateOnly ClaimDate, + decimal GrossCost, + decimal Tax, + string CostCentre, + ExpenseClaimStatus Status) : IExpenseClaimSubmission; +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryManagerLookupService.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryManagerLookupService.cs new file mode 100644 index 0000000..00a65bc --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Domain/InMemoryManagerLookupService.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Identity; + +namespace WebApp.Domain; + +public sealed class InMemoryManagerLookupService(UserManager userManager) : IManagerLookupService +{ + private static readonly IReadOnlyDictionary ManagerUserNameMap = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["alice"] = "bob", + ["bob"] = "alice" + }; + + public async Task GetManagerUserId(string submitterUserId) + { + if (string.IsNullOrWhiteSpace(submitterUserId)) + { + throw new ArgumentException("Submitter user id is required.", nameof(submitterUserId)); + } + + var submitter = await userManager.FindByIdAsync(submitterUserId.Trim()); + if (submitter is null || string.IsNullOrWhiteSpace(submitter.UserName)) + { + throw new InvalidOperationException($"Submitter user '{submitterUserId}' was not found."); + } + + if (!ManagerUserNameMap.TryGetValue(submitter.UserName, out var managerUserName)) + { + throw new InvalidOperationException($"No manager mapping exists for '{submitter.UserName}'."); + } + + var manager = await userManager.FindByNameAsync(managerUserName); + if (manager is null) + { + throw new InvalidOperationException($"Mapped manager '{managerUserName}' was not found."); + } + + return manager.Id; + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml new file mode 100644 index 0000000..60c8bdd --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml @@ -0,0 +1,24 @@ +@page "/AccessDenied" +@model WebApp.Pages.AccessDeniedModel +@{ + ViewData["Title"] = "Access Denied"; +} + +
+
+
+
+

Access Denied

+ @foreach (string message in @Model.Messages) + { +

@message

+ } + +
+
+
+
+ diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml.cs new file mode 100644 index 0000000..00fa250 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/AccessDenied.cshtml.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using WebApp.Domain; + +namespace WebApp.Pages; + +public class AccessDeniedModel(IGenerateAccessDeniedContent accessDeniedContent) : PageModel +{ + [BindProperty(SupportsGet = true)] + public string? Reason { get; set; } + + public IEnumerable Messages => accessDeniedContent.Messages(Reason ?? ""); + + public void OnGet() + { + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml new file mode 100644 index 0000000..9a7a3bb --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml @@ -0,0 +1,47 @@ +@page +@model WebApp.Pages.Account.LoginModel +@{ + ViewData["Title"] = "Sign in"; +} + +
+
+
+
+

Sign in

+ + @if (!string.IsNullOrWhiteSpace(Model.ErrorMessage)) + { + + } + +
+
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+ +

+ Try alice or bob with password Passw0rd! +

+
+
+
+
diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..64815fa --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Login.cshtml.cs @@ -0,0 +1,59 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace WebApp.Pages.Account; + +public class LoginModel(SignInManager signInManager) : PageModel +{ + private readonly SignInManager _signInManager = signInManager; + + [BindProperty] + public InputModel Input { get; set; } = new(); + + public string? ReturnUrl { get; set; } + public string? ErrorMessage { get; set; } + + public void OnGet(string? returnUrl = null) + { + ReturnUrl = string.IsNullOrWhiteSpace(returnUrl) ? "/Home" : returnUrl; + } + + public async Task OnPostAsync(string? returnUrl = null) + { + ReturnUrl = string.IsNullOrWhiteSpace(returnUrl) ? "/Home" : returnUrl; + + if (!ModelState.IsValid) + { + return Page(); + } + + var result = await _signInManager.PasswordSignInAsync( + Input.UserName, + Input.Password, + Input.RememberMe, + lockoutOnFailure: false); + + if (result.Succeeded) + { + return LocalRedirect(ReturnUrl); + } + + ErrorMessage = "Invalid username or password."; + return Page(); + } + + public sealed class InputModel + { + [Required] + public string UserName { get; set; } = string.Empty; + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = string.Empty; + + public bool RememberMe { get; set; } + } +} + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml new file mode 100644 index 0000000..1bf23c0 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml @@ -0,0 +1,19 @@ +@page +@model WebApp.Pages.Account.Logout + +@{ + Layout = null; +} + + + + + + + + +
+ +
+ + \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml.cs new file mode 100644 index 0000000..e2293c2 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Account/Logout.cshtml.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace WebApp.Pages.Account; + +public class Logout(SignInManager signInManager) : PageModel +{ +private readonly SignInManager _signInManager = signInManager; + +public async Task OnPostAsync() +{ + await _signInManager.SignOutAsync(); + return RedirectToPage("/Home"); +} +} \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml new file mode 100644 index 0000000..19fea27 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml @@ -0,0 +1,72 @@ +@page +@model WebApp.Pages.Expenses.ApproveModel +@{ + ViewData["Title"] = "Approve Claims"; +} + +
+
+
+
+

Claims Assigned To You

+ + @if (!string.IsNullOrWhiteSpace(Model.StatusMessage)) + { + + } + + @if (Model.Claims.Count == 0) + { +

No submitted claims are currently assigned to you.

+ } + else + { +
+
+ + + + + + + + + + + + + + + @foreach (var claim in Model.Claims) + { + + + + + + + + + + + } + +
DateSubmitterDescriptionGrossTaxCost CentreStatus
+ + @claim.ClaimDate.ToString("yyyy-MM-dd")@claim.SubmitterUserId@claim.Description@claim.GrossCost.ToString("C")@claim.Tax.ToString("C")@claim.CostCentre + @claim.Status +
+
+ +
+ + + Back +
+
+ } +
+
+
+
+ diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml.cs new file mode 100644 index 0000000..02dff86 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Approve.cshtml.cs @@ -0,0 +1,122 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using WebApp.Authorization; +using WebApp.Domain; + +namespace WebApp.Pages.Expenses; + +[Authorize] +public class ApproveModel(IExpenseClaimService expenseClaimService, IGenerateAccessDeniedContent accessDenied, IAuthorizeExpenseClaimActions pep) : PageModel +{ + [BindProperty] + public List SelectedClaimIds { get; set; } = []; + + [BindProperty] + public string Action { get; set; } = string.Empty; + + public IReadOnlyList Claims { get; private set; } = []; + + public string? StatusMessage { get; private set; } + + public async Task OnGet() + { + var approverUserId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrWhiteSpace(approverUserId)) + { + return Challenge(); + } + + AuthorizeResult canApproveAndRejectClaims = await pep.CanApproveAndRejectClaims(approverUserId); + if (!canApproveAndRejectClaims.Success) + { + return accessDenied.Redirect(canApproveAndRejectClaims.Messages); + } + + await LoadExpenseClaims(approverUserId); + return Page(); + } + + public async Task OnPost() + { + var approverUserId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrWhiteSpace(approverUserId)) + { + return Challenge(); + } + + var action = Action?.Trim().ToLowerInvariant(); + if (action is not ("approve" or "reject")) + { + StatusMessage = "Choose an action."; + await LoadExpenseClaims(approverUserId); + return Page(); + } + + var assignedClaims = await expenseClaimService.GetByApproverUserId(approverUserId); + Dictionary allowedIds = assignedClaims + .Where(c => c.Status == ExpenseClaimStatus.Submitted) + .ToDictionary(e => e.SubmissionId,e=>e); + + List selectedIds = SelectedClaimIds + .Where(id => !string.IsNullOrWhiteSpace(id) && allowedIds.ContainsKey(id)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + List expensesToActUpon = assignedClaims.Where(c => selectedIds.Contains(c.SubmissionId)).ToList(); + + Func, Task> authorizationCall = + action == "approve" ? pep.CanApproveClaims : pep.CanRejectClaims; + + AuthorizeResult claimActionAuthorizationResult = await authorizationCall(approverUserId, expensesToActUpon ); + + if (!claimActionAuthorizationResult.Success) + { + return accessDenied.Redirect(claimActionAuthorizationResult.Messages); + } + + foreach (var submissionId in selectedIds) + { + var nextStatus = action == "approve" ? ExpenseClaimStatus.Approved : ExpenseClaimStatus.Rejected; + await expenseClaimService.UpdateStatus(submissionId, nextStatus); + } + + StatusMessage = selectedIds.Count == 0 + ? "No claims were selected." + : $"{(action == "approve" ? "Approved" : "Rejected")} {selectedIds.Count} claim(s)."; + + SelectedClaimIds.Clear(); + await LoadExpenseClaims(approverUserId); + return Page(); + } + + private async Task LoadExpenseClaims(string approverUserId) + { + var claims = await expenseClaimService.GetByApproverUserId(approverUserId); + + Claims = claims + .Where(c => c.Status == ExpenseClaimStatus.Submitted) + .OrderBy(c => c.ClaimDate) + .Select(c => new PendingApprovalViewModel( + c.SubmissionId, + c.SubmitterUserId, + c.Description, + c.ClaimDate, + c.GrossCost, + c.Tax, + c.CostCentre, + c.Status)) + .ToList(); + } + + public sealed record PendingApprovalViewModel( + string SubmissionId, + string SubmitterUserId, + string Description, + DateOnly ClaimDate, + decimal GrossCost, + decimal Tax, + string CostCentre, + ExpenseClaimStatus Status); +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml new file mode 100644 index 0000000..70048b7 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml @@ -0,0 +1,108 @@ +@page +@model WebApp.Pages.Expenses.CreateModel +@{ + ViewData["Title"] = "Submit Expense Claim"; +} + +
+
+
+
+

Submit Expense Claim

+

Enter expense details and review your open claims below.

+ + @if (Model.Submitted) + { + + } + +
+
+ + + +
+ +
+ + + +
+ +
+
+ + + +
+
+ + + +
+
+ +
+ + + +
+ +
+ + Cancel +
+
+
+
+ +
+
+

Your Open Claims

+ + @if (Model.ExistingClaims.Count == 0) + { +

You have no submitted or rejected claims.

+ } + else + { +
+ + + + + + + + + + + + + @foreach (var claim in Model.ExistingClaims) + { + + + + + + + + + } + +
DateDescriptionGrossTaxCost CentreStatus
@claim.ClaimDate.ToString("yyyy-MM-dd")@claim.Description@claim.GrossCost.ToString("C")@claim.Tax.ToString("C")@claim.CostCentre + + @claim.Status + +
+
+ } +
+
+
+
diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml.cs new file mode 100644 index 0000000..4c53d6c --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Expenses/Create.cshtml.cs @@ -0,0 +1,148 @@ +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using WebApp.Authorization; +using WebApp.Domain; + +namespace WebApp.Pages.Expenses; + +[Authorize] +public class CreateModel( + IExpenseClaimService expenseClaimService, + IAuthorizeExpenseClaimActions authorizeExpenseClaimActions, + IGenerateAccessDeniedContent accessDeniedService) : PageModel +{ + [BindProperty] + public ExpenseClaimSubmissionModel Submission { get; set; } = new(); + + public bool Submitted { get; private set; } + + public IReadOnlyList ExistingClaims { get; private set; } = []; + + public IEnumerable CostCentres => + [ + new("G&A", "G&A"), + new("R&D", "R&D"), + new("Sales", "Sales"), + new("PS", "PS"), + new("Maintenance", "Maintenance") + ]; + + public async Task OnGet() + { + Submission.ClaimDate = DateOnly.FromDateTime(DateTime.Today); + + var submitterUserId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrWhiteSpace(submitterUserId)) + { + return Challenge(); + } + + + AuthorizeResult canCreateAuthorizationResult = await authorizeExpenseClaimActions.CanCreateClaim(submitterUserId); + if (!canCreateAuthorizationResult.Success) + { + return accessDeniedService.Redirect(canCreateAuthorizationResult.Messages); + } + + await LoadExistingClaims(submitterUserId); + return Page(); + } + + public async Task OnPost() + { + var submitterUserId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrWhiteSpace(submitterUserId)) + { + return Challenge(); + } + + if (!ModelState.IsValid) + { + await LoadExistingClaims(submitterUserId); + return Page(); + } + + var canSubmit = await authorizeExpenseClaimActions.CanSubmitClaim( + submitterUserId, + Submission.GrossCost!.Value); + if (!canSubmit.Success) + { + return accessDeniedService.Redirect(canSubmit.Messages); + } + + await expenseClaimService.Create( + submitterUserId, + Submission.Description, + Submission.ClaimDate!.Value, + Submission.GrossCost!.Value, + Submission.Tax!.Value, + Submission.CostCentre); + + Submitted = true; + ModelState.Clear(); + Submission = new ExpenseClaimSubmissionModel + { + ClaimDate = DateOnly.FromDateTime(DateTime.Today) + }; + + await LoadExistingClaims(submitterUserId); + return Page(); + } + + private async Task LoadExistingClaims(string submitterUserId) + { + var claims = await expenseClaimService.GetBySubmitterUserId(submitterUserId); + ExistingClaims = claims + .Where(c => c.Status != ExpenseClaimStatus.Approved) + .OrderByDescending(c => c.ClaimDate) + .Select(c => new OpenExpenseClaimViewModel( + c.SubmissionId, + c.Description, + c.ClaimDate, + c.GrossCost, + c.Tax, + c.CostCentre, + c.Status)) + .ToList(); + } + + public sealed class ExpenseClaimSubmissionModel + { + [Required] + [StringLength(200)] + [Display(Name = "Description")] + public string Description { get; set; } = string.Empty; + + [Required] + [DataType(DataType.Date)] + [Display(Name = "Claim Date")] + public DateOnly? ClaimDate { get; set; } + + [Required] + [Range(typeof(decimal), "0.01", "79228162514264337593543950335")] + [Display(Name = "Gross Cost")] + public decimal? GrossCost { get; set; } + + [Required] + [Range(typeof(decimal), "0.00", "79228162514264337593543950335")] + [Display(Name = "Tax")] + public decimal? Tax { get; set; } + + [Required] + [Display(Name = "Cost Centre")] + public string CostCentre { get; set; } = string.Empty; + } + + public sealed record OpenExpenseClaimViewModel( + string SubmissionId, + string Description, + DateOnly ClaimDate, + decimal GrossCost, + decimal Tax, + string CostCentre, + ExpenseClaimStatus Status); +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml new file mode 100644 index 0000000..74245ce --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml @@ -0,0 +1,45 @@ +@page "/Home" +@model WebApp.Pages.HomeModel +@{ + ViewData["Title"] = "Home"; +} + +
+
+
+
+

Expense Claims App

+

+ This application helps employees submit expense claims and enables assigned managers + to review and approve or reject claims. +

+ +

What You Can Do

+
    +
  • Create claims: Submit expense details including description, date, cost values, and cost centre.
  • +
  • Track open claims: View your submitted and rejected claims that are still open.
  • +
  • Approve assigned claims: If you are an approver, review claims assigned to you and process many at once.
  • +
+ +

How Approval Works

+

+ When a claim is submitted, the service assigns it to an approver using manager lookup. + Approvers see only claims assigned to them on the Approve page. +

+ +

Authorization Rules

+
    +
  • bob is an employee, he can submit expense claims
  • +
  • expense claims have a limit of 1000
  • +
  • alice is bob's manager she can approve or reject bobs expense claims
  • +
+ + +
+
+
+
+ diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml.cs new file mode 100644 index 0000000..6f54817 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Home.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace WebApp.Pages; + +public class HomeModel() : PageModel +{ + +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_Layout.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..726175e --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_Layout.cshtml @@ -0,0 +1,84 @@ + + + + + + + @ViewData["Title"] - WebApp + + + + + + +
+ @RenderBody() +
+ + + + + + +@using Microsoft.AspNetCore.Identity +@inject SignInManager SignInManager +@{ + var isAuthenticated = User.Identity?.IsAuthenticated == true; + var userName = User.Identity?.Name; + + if (isAuthenticated) + { + IdentityUser? knownUser = null; + + if (!string.IsNullOrWhiteSpace(userName)) + { + knownUser = await SignInManager.UserManager.FindByNameAsync(userName); + } + + if (knownUser is null) + { + await SignInManager.SignOutAsync(); + Context.Response.Redirect("/Account/Login"); + return; + } + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_ValidationScriptsPartial.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..16118f8 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,4 @@ + + + + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_Layout.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_Layout.cshtml new file mode 100644 index 0000000..139597f --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_Layout.cshtml @@ -0,0 +1,2 @@ + + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewImports.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..c6b4f01 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using WebApp +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@namespace WebApp.Pages + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewStart.cshtml b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..c75368d --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "Shared/_Layout"; +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Program.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Program.cs new file mode 100644 index 0000000..1860e1d --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Program.cs @@ -0,0 +1,102 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Rsk.AuthZen.Client; +using WebApp; +using WebApp.Authorization; +using WebApp.Domain; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => + options.UseInMemoryDatabase("AuthZenIdentity")); + +builder.Services + .AddIdentityApiEndpoints(options => + { + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 6; + }) + .AddEntityFrameworkStores(); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme; + options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme; + options.DefaultSignInScheme = IdentityConstants.ApplicationScheme; +}); + +builder.Services.ConfigureApplicationCookie(options => +{ + options.LoginPath = "/Account/Login"; + options.AccessDeniedPath = "/AccessDenied"; + options.ReturnUrlParameter = "returnUrl"; +}); + +builder.Services.AddAuthorization(); +builder.Services.AddRazorPages(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +builder.Services.AddHttpClient(); + +builder.Services.Configure(options => +{ + options.AuthorizationUrl = "https://localhost:7064"; +}); +builder.Services.AddTransient(); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/", () => Results.Redirect("/Home")); + +app.MapRazorPages(); +app.MapIdentityApi(); + +await SeedUsersAsync(app.Services); + +app.Run(); + +static async Task SeedUsersAsync(IServiceProvider services) +{ + using var scope = services.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + await EnsureUserAsync(userManager, "alice", "alice@example.local", "Passw0rd!"); + await EnsureUserAsync(userManager, "bob", "bob@example.local", "Passw0rd!"); +} + +static async Task EnsureUserAsync(UserManager userManager, string userName, string email, string password) +{ + var existing = await userManager.FindByNameAsync(userName); + if (existing is not null) + { + return; + } + + var user = new IdentityUser + { + UserName = userName, + Email = email, + EmailConfirmed = true, + Id = userName + }; + + + var result = await userManager.CreateAsync(user, password); + + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + throw new InvalidOperationException($"Failed to seed user '{userName}': {errors}"); + } + +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Properties/launchSettings.json b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Properties/launchSettings.json new file mode 100644 index 0000000..3744f5e --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5062", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7255;http://localhost:5062", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md b/Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md new file mode 100644 index 0000000..486689f --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md @@ -0,0 +1,23 @@ +# WebApp + +This app uses ASP.NET Core Identity with an in-memory EF Core store and includes a Razor Pages sign-in flow. + +## Seeded users + +- `alice` / `Passw0rd!` +- `bob` / `Passw0rd!` + +## Run + +```zsh +cd /Users/andyclymer/git/AuthZenExample/WebApp +dotnet run +``` + +Then open the printed local URL and go to `/Account/Login`. + +## Notes + +- User data is in-memory only; restarting the app resets users and sessions. +- `/secure` requires authentication. + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/WebApp.csproj b/Samples/CSharp/PolicyDrivenExpenses/WebApp/WebApp.csproj new file mode 100644 index 0000000..a71b3af --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/WebApp.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + + + + + + + + + diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.Development.json b/Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.json b/Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From c479a55d0c45bd043684e97baf2d8dc3c51b1bd5 Mon Sep 17 00:00:00 2001 From: andrewclymer Date: Fri, 10 Apr 2026 14:37:08 +0100 Subject: [PATCH 30/33] Added location of where to get an Enforcer license key --- .../CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs index 2763256..3ba4562 100644 --- a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/Program.cs @@ -15,7 +15,7 @@ public static void Main(string[] args) .AddEnforcer("acmeCorp.global",options => { options.Licensee = "DEMO"; - options.LicenseKey = "eyJhdXRoIjoiREVNTyIsImV4cCI6IjIwMjYtMDQtMzBUMDA6MDA6MDAiLCJpYXQiOiIyMDI2LTAzLTMwVDExOjUwOjA1LjQxMDU5NTFaIiwib3JnIjoiREVNTyIsImF1ZCI6N30=.mtYt37KGtQFJ5je0XJGckWrOx6lqCF5QwraMPJyGFgzYOq8sAFARoIjCKpJ0JpbpCRbCcaTFFhekfHU6NLvJka/ZzfsOYM4JHBSQpol2Z38PwkR4p8J6ONBi/SYOIvXrTk48Tf09Tvo2WHeoiZ9/MLu4IN7+w8sib0fUdkt/cY1PKHHzofBBHPsOT4/LOyxZoVIFLsINC5IOCkGf1vkmCADVFTszOY5nwUf3CNBs+C6UwfpHnvggnMnZpanW45WoWDDcQHgxwS13LgH6k+0XBUrPdcFhTR9mlSuboDspctvVeNASUBWcSLLdGY7GhK2RAWEAf9bbsTrSHErqIK+gx0XcDaq+n94q/qW3swJGGjUlcj+PaGPhmoEojYfwFWWZU6y4dz45XC941GpsYZEGYSVos5+oJMdreCOZqoPXhjEiqmRDgNT7llQ4bixr9voW3N1WKrfy6Ftr2ZYPv/tSOZb3wofGkpLSpPAw/XiyWUOkIiuVajR9CM8//pWQCOZodL1/xuXlioW8EVECXoGDhreDaGhc5BIEycJC/Fv0rgrnFxrPbStm8z+jmigGhN7G7quXaZr+VHhr+WEgjqbB3MSUhR1f/jwjKtiQMEoU7EDiC9BsNkV+KmGKr+o23HlvM2mwE5/rOa/ORgJ3LZmad2yBi6CYge8lwmSLWABMEmc="; + options.LicenseKey = "Get a free license from https://www.identityserver.com/products/enforcer"; }) .AddPolicyEnforcementPoint(o => o.Bias = PepBias.Deny) .AddAuthZen() From 9942f3c7a006cd81b3da351cdcb37acfb47c7f32 Mon Sep 17 00:00:00 2001 From: andrewclymer Date: Mon, 13 Apr 2026 12:53:54 +0100 Subject: [PATCH 31/33] Fixed comments on PR --- .../AuthZenPolicyServer/AuthZenPolicyServer.csproj | 6 ------ .../AuthZenPolicyServer/SubjectAttributeProvider.cs | 12 ++++++++---- .../AuthZenAuthorizeExpenseClaimActions.cs | 9 +++++++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj index 92cfa97..4ea04e3 100644 --- a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/AuthZenPolicyServer.csproj @@ -15,10 +15,4 @@ - - - - ..\..\..\..\..\usr\local\share\dotnet\shared\Microsoft.AspNetCore.App\10.0.3\Microsoft.AspNetCore.dll - - diff --git a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs index 4404532..90f8ef5 100644 --- a/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs +++ b/Samples/CSharp/PolicyDrivenExpenses/AuthZenPolicyServer/SubjectAttributeProvider.cs @@ -17,8 +17,9 @@ public class SubjectAttributeProvider : RecordAttributeValueProvider GetRecordValue(IAttributeResolver attributeResolver, CancellationToken ct) + + protected override async Task GetRecordValue(IAttributeResolver attributeResolver, + CancellationToken ct) { IReadOnlyCollection? identifiers = await attributeResolver .Resolve(Rsk.Enforcer.Oasis.Attributes.Subject.Identifier, ct); @@ -26,8 +27,11 @@ protected override async Task GetRecordValue(IAttributeResolver string? identifier = identifiers.SingleOrDefault(); if (identifier == null) return null!; - AcmeCorpPerson person = new AcmeCorpPerson(); + if (people.TryGetValue(identifier, out AcmeCorpPerson? person)) + { + return person; + } - return people[identifier]; + return null!; } } \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs index 5182c1e..86a1d1b 100644 --- a/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/Authorization/AuthZenAuthorizeExpenseClaimActions.cs @@ -97,8 +97,13 @@ private async Task Authorize(AuthZenEvaluationRequest request) if (success == false) { - string? error = - JsonSerializer.Deserialize(response.Context).GetProperty("error").GetString(); + string? error = null; + + if (JsonSerializer.Deserialize(response.Context) + .TryGetProperty("error", out JsonElement errorProperty)) + { + error = errorProperty.GetString(); + } return new AuthorizeResult(false, [error ?? String.Empty]); } From 5ae785d8fefeb2e500fe7ff8687e03c9b5685504 Mon Sep 17 00:00:00 2001 From: andrewclymer Date: Tue, 14 Apr 2026 11:25:56 +0100 Subject: [PATCH 32/33] Updated readme --- Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md b/Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md index 486689f..47d66bc 100644 --- a/Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md +++ b/Samples/CSharp/PolicyDrivenExpenses/WebApp/README.md @@ -10,14 +10,11 @@ This app uses ASP.NET Core Identity with an in-memory EF Core store and includes ## Run ```zsh -cd /Users/andyclymer/git/AuthZenExample/WebApp dotnet run ``` -Then open the printed local URL and go to `/Account/Login`. +Then and open https://localhost:7255 ## Notes - - User data is in-memory only; restarting the app resets users and sessions. -- `/secure` requires authentication. From 5046269235d3ef38c385031993c7380277cee2f3 Mon Sep 17 00:00:00 2001 From: andrewclymer Date: Tue, 14 Apr 2026 17:59:09 +0100 Subject: [PATCH 33/33] Removing unneccessary files --- .gitignore | 6 ++---- .../.idea.PolicyDrivenExpenses/.idea/.gitignore | 15 --------------- .../.idea/copilot.data.migration.agent.xml | 6 ------ .../.idea/copilot.data.migration.ask.xml | 6 ------ .../.idea/copilot.data.migration.ask2agent.xml | 6 ------ .../.idea/copilot.data.migration.edit.xml | 6 ------ .../.idea/encodings.xml | 4 ---- .../.idea/indexLayout.xml | 8 -------- .../.idea.PolicyDrivenExpenses/.idea/vcs.xml | 7 ------- .../PolicyDrivenExpenses.sln.DotSettings.user | 16 ---------------- 10 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml delete mode 100644 Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user diff --git a/.gitignore b/.gitignore index ca8de4d..41e5106 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,7 @@ riderModule.iml /_ReSharper.Caches/ **/.DS_Store -src/CSharp/.idea/.idea.Rsk.AuthZen/.idea/ - +.idea +*.DotSettings.user src/Typescript/dist/ src/Typescript/node_modules - -src/CSharp/.idea/ diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore deleted file mode 100644 index a3db6dc..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/.idea.AuthZenExample.iml -/modules.xml -/projectSettingsUpdater.xml -/contentModel.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml deleted file mode 100644 index 4ea72a9..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml deleted file mode 100644 index 7ef04e2..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml deleted file mode 100644 index 1f2ea11..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.ask2agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml deleted file mode 100644 index 8648f94..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/copilot.data.migration.edit.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml b/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml deleted file mode 100644 index ba43f69..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/.idea/.idea.PolicyDrivenExpenses/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user b/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user deleted file mode 100644 index 0f4cf32..0000000 --- a/Samples/CSharp/PolicyDrivenExpenses/PolicyDrivenExpenses.sln.DotSettings.user +++ /dev/null @@ -1,16 +0,0 @@ - - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded \ No newline at end of file