diff --git a/SlimFaas.sln b/SlimFaas.sln index 171fa1bbe..d3702e6d0 100644 --- a/SlimFaas.sln +++ b/SlimFaas.sln @@ -22,12 +22,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GmailMailerApi", "src\Gmail EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalculatorApi", "src\CalculatorApi\CalculatorApi.csproj", "{C677E191-12D7-4F45-8777-32438A92CC35}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "src\ConsoleApp1\ConsoleApp1.csproj", "{FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimFaasKafka", "src\SlimFaasKafka\SlimFaasKafka.csproj", "{D771C816-4642-443B-A155-7DCA8C130864}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimFaasKafka.Tests", "tests\SlimFaasKafka.Tests\SlimFaasKafka.Tests.csproj", "{CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlimFaasMcpGateway", "src\SlimFaasMcpGateway\SlimFaasMcpGateway.csproj", "{6FFC9F83-0529-487E-901D-92FB73E3EC21}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -78,10 +78,6 @@ Global {C677E191-12D7-4F45-8777-32438A92CC35}.Debug|Any CPU.Build.0 = Debug|Any CPU {C677E191-12D7-4F45-8777-32438A92CC35}.Release|Any CPU.ActiveCfg = Release|Any CPU {C677E191-12D7-4F45-8777-32438A92CC35}.Release|Any CPU.Build.0 = Release|Any CPU - {FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB416D23-6DD2-450F-B2DB-DDEFC8B39B61}.Release|Any CPU.Build.0 = Release|Any CPU {D771C816-4642-443B-A155-7DCA8C130864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D771C816-4642-443B-A155-7DCA8C130864}.Debug|Any CPU.Build.0 = Debug|Any CPU {D771C816-4642-443B-A155-7DCA8C130864}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -90,5 +86,9 @@ Global {CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE2BCA73-D7DB-4D9E-8902-4E88A6926CDB}.Release|Any CPU.Build.0 = Release|Any CPU + {6FFC9F83-0529-487E-901D-92FB73E3EC21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FFC9F83-0529-487E-901D-92FB73E3EC21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FFC9F83-0529-487E-901D-92FB73E3EC21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FFC9F83-0529-487E-901D-92FB73E3EC21}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/global.json b/global.json index e6d85eca4..82dabbbf1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.102", + "version": "10.0.100", "rollForward": "latestFeature", "allowPrerelease": true } diff --git a/src/CalculatorApi/Properties/launchSettings.json b/src/CalculatorApi/Properties/launchSettings.json index 7b119bfe7..9bf2e80c1 100644 --- a/src/CalculatorApi/Properties/launchSettings.json +++ b/src/CalculatorApi/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5106", + "applicationUrl": "http://localhost:5107", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/ConsoleApp1/ConsoleApp1.csproj b/src/ConsoleApp1/ConsoleApp1.csproj deleted file mode 100644 index 6c1dc922b..000000000 --- a/src/ConsoleApp1/ConsoleApp1.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - diff --git a/src/ConsoleApp1/Program.cs b/src/ConsoleApp1/Program.cs deleted file mode 100644 index 76cdabeef..000000000 --- a/src/ConsoleApp1/Program.cs +++ /dev/null @@ -1,56 +0,0 @@ -// dotnet run -// .NET 7/8/9 - -using System.Net.Http; -using System.Text; -using System.Text.Json; - -var baseAddress = "http://localhost:30021"; -var fib1 = "/async-function/fibonacci1/compute"; -var fib2 = "/async-function/fibonacci2/compute"; - -// Combien de paires (2 requêtes envoyées en même temps : fib1 + fib2) -int pairs = 10000; - -// (Optionnel) délai entre paires pour bien voir les logs côté serveur -TimeSpan? interPairDelay = TimeSpan.FromMilliseconds(10); - -using var http = new HttpClient { BaseAddress = new Uri(baseAddress) }; - -for (int i = 0; i < pairs; i++) -{ - // Prépare 2 bodies différents - var body1 = JsonSerializer.Serialize(new { seq = i, type = "fib1", n = 30, note = "pair-start" }); - var body2 = JsonSerializer.Serialize(new { seq = i, type = "fib2", n = 31, note = "pair-start" }); - - // Crée les 2 requêtes - using var req1 = new HttpRequestMessage(HttpMethod.Post, fib1) - { - Content = new StringContent(body1, Encoding.UTF8, "application/json") - }; - using var req2 = new HttpRequestMessage(HttpMethod.Post, fib2) - { - Content = new StringContent(body2, Encoding.UTF8, "application/json") - }; - - Console.WriteLine($"[{i}] -> (parallel) POST {fib1} | {fib2}"); - - // Lance vraiment en parallèle - var send1 = http.SendAsync(req1); - var send2 = http.SendAsync(req2); - - var responses = await Task.WhenAll(send1, send2); - - // (Optionnel) log des réponses - for (int k = 0; k < responses.Length; k++) - { - var r = responses[k]; - var txt = await r.Content.ReadAsStringAsync(); - Console.WriteLine($"[{i}] <- {(k==0 ? "fib1" : "fib2")} {((int)r.StatusCode)} {r.ReasonPhrase} body={txt}"); - } - - if (interPairDelay is not null) - await Task.Delay(interPairDelay.Value); -} - -Console.WriteLine("Done."); diff --git a/src/SlimFaas/Endpoints/EventEndpoints.cs b/src/SlimFaas/Endpoints/EventEndpoints.cs index 82c4abeac..a6a32bd89 100644 --- a/src/SlimFaas/Endpoints/EventEndpoints.cs +++ b/src/SlimFaas/Endpoints/EventEndpoints.cs @@ -98,7 +98,7 @@ private static async Task PublishEvent( string baseFunctionPodUrl = Environment.GetEnvironmentVariable(EnvironmentVariables.BaseFunctionPodUrl) ?? EnvironmentVariables.BaseFunctionPodUrlDefault; - var baseUrl = SlimDataEndpoint.Get(pod, baseFunctionPodUrl); + var baseUrl = SlimDataEndpoint.Get(pod, baseFunctionPodUrl) + "/"; logger.LogDebug("Sending event {EventName} to {FunctionDeployment} at {BaseUrl} with path {FunctionPath} and query {UriComponent}", eventName, function.Deployment, baseUrl, functionPath, context.Request.QueryString.ToUriComponent()); diff --git a/src/SlimFaasMcp/ClientApp/src/pages/swagger/SwaggerToMcpPage.tsx b/src/SlimFaasMcp/ClientApp/src/pages/swagger/SwaggerToMcpPage.tsx index aa7d2d990..40eab5a6d 100644 --- a/src/SlimFaasMcp/ClientApp/src/pages/swagger/SwaggerToMcpPage.tsx +++ b/src/SlimFaasMcp/ClientApp/src/pages/swagger/SwaggerToMcpPage.tsx @@ -7,7 +7,7 @@ import type { McpTool, UiTool } from "./types"; import { fromBase64Utf8, isProbablyBase64, safeJsonParse, toBase64Utf8 } from "../../utils/encoding"; import "./SwaggerToMcpPage.scss"; -const API_BASE = (import.meta.env.VITE_MCP_API_BASE_URL ?? "").replace(/\/+$/, ""); +const API_BASE = (import.meta.env.VITE_MCP_API_BASE_URL ?? "").replace(/[/.]+$/, ""); const api = (path: string) => (API_BASE ? `${API_BASE}${path}` : path); function clampUrlBase(baseUrl: string): string { @@ -140,7 +140,10 @@ export default function SwaggerToMcpPage() { let url = `${originBase}/mcp?openapi_url=${encodeURIComponent(swaggerUrl.trim())}`; const b = clampUrlBase(baseUrl.trim()); if (b) url += `&base_url=${encodeURIComponent(b)}`; - if (promptB64 && isProbablyBase64(promptB64)) url += `&mcp_prompt=${encodeURIComponent(promptB64)}`; + // Ne pas ajouter mcp_prompt si c'est un objet JSON vide (e30= en base64) + if (promptB64 && isProbablyBase64(promptB64) && promptB64 !== 'e30=') { + url += `&mcp_prompt=${encodeURIComponent(promptB64)}`; + } if (includePrm && oauthB64 && isProbablyBase64(oauthB64)) url += `&oauth=${encodeURIComponent(oauthB64)}`; if (structuredContent) url += `&structured_content=true`; if (toolPrefix.trim()) url += `&tool_prefix=${encodeURIComponent(toolPrefix.trim())}`; @@ -308,7 +311,7 @@ export default function SwaggerToMcpPage() { let endpoint = `/tools/${encodeURIComponent(toolName)}?openapi_url=${encodeURIComponent(swagger)}`; if (b) endpoint += `&base_url=${encodeURIComponent(b)}`; - if (promptB64) endpoint += `&mcp_prompt=${encodeURIComponent(promptB64)}`; + if (promptB64 && promptB64 !== 'e30=') endpoint += `&mcp_prompt=${encodeURIComponent(promptB64)}`; if (includePrm && oauthB64) endpoint += `&oauth=${encodeURIComponent(oauthB64)}`; if (structuredContent) endpoint += `&structured_content=true`; if (toolPrefix.trim()) endpoint += `&tool_prefix=${encodeURIComponent(toolPrefix.trim())}`; diff --git a/src/SlimFaasMcpGateway/Audit/JsonPatch.cs b/src/SlimFaasMcpGateway/Audit/JsonPatch.cs new file mode 100644 index 000000000..4fa636feb --- /dev/null +++ b/src/SlimFaasMcpGateway/Audit/JsonPatch.cs @@ -0,0 +1,183 @@ + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace SlimFaasMcpGateway.Audit; + +/// +/// A minimal JSON patch mechanism (not RFC6902) designed for: +/// - storing "diffs" between JSON snapshots, +/// - reconstructing snapshots by applying patches in order. +/// Arrays are treated as atomic values (whole array replace). +/// +public static class JsonPatch +{ + public sealed record Op(string Path, JsonNode? Value, bool Remove); + + public static IReadOnlyList Create(JsonNode? before, JsonNode? after) + { + var ops = new List(); + DiffInternal(ops, "", before, after); + return ops; + } + + public static JsonNode Apply(JsonNode baseNode, IEnumerable ops) + { + foreach (var op in ops) + { + ApplyOne(baseNode, op); + } + return baseNode; + } + + public static string Serialize(IReadOnlyList ops) + => JsonSerializer.Serialize(ops, AppJsonOptions.Default); + + public static IReadOnlyList Deserialize(string json) + => JsonSerializer.Deserialize>(json, AppJsonOptions.Default) ?? new(); + + /// + /// Creates a text-based unified diff between two JSON nodes using DiffPlex + /// + public static TextDiff.UnifiedDiff CreateTextDiff(JsonNode? before, JsonNode? after) + { + var beforeText = before?.ToJsonString(AppJsonOptions.DefaultIndented) ?? ""; + var afterText = after?.ToJsonString(AppJsonOptions.DefaultIndented) ?? ""; + return TextDiff.Create(beforeText, afterText); + } + + private static void DiffInternal(List ops, string path, JsonNode? before, JsonNode? after) + { + if (JsonEquals(before, after)) + return; + + // null cases + if (before is null) + { + ops.Add(new Op(path, after, Remove: false)); + return; + } + + if (after is null) + { + ops.Add(new Op(path, Value: null, Remove: true)); + return; + } + + // arrays -> whole replace + if (before is JsonArray || after is JsonArray) + { + ops.Add(new Op(path, after, Remove: false)); + return; + } + + // objects -> key-level diff + if (before is JsonObject bObj && after is JsonObject aObj) + { + // removed keys + foreach (var kv in bObj) + { + if (!aObj.ContainsKey(kv.Key)) + ops.Add(new Op(Join(path, kv.Key), Value: null, Remove: true)); + } + + // added/changed keys + foreach (var kv in aObj) + { + bObj.TryGetPropertyValue(kv.Key, out var bVal); + DiffInternal(ops, Join(path, kv.Key), bVal, kv.Value); + } + + return; + } + + // primitive replace + ops.Add(new Op(path, after, Remove: false)); + } + + private static void ApplyOne(JsonNode root, Op op) + { + if (string.IsNullOrEmpty(op.Path)) + { + // root replace is handled by caller (we keep it simple: replace properties instead) + if (root is JsonObject rObj && op.Value is JsonObject vObj && !op.Remove) + { + rObj.Clear(); + foreach (var kv in vObj) rObj[kv.Key] = kv.Value; + return; + } + + throw new InvalidOperationException("Root replace not supported for non-object roots."); + } + + var (parent, lastSeg) = NavigateToParent(root, op.Path); + + if (parent is JsonObject pobj) + { + if (op.Remove) pobj.Remove(lastSeg); + else pobj[lastSeg] = op.Value?.DeepClone(); + return; + } + + throw new InvalidOperationException($"Unsupported parent node for path '{op.Path}'."); + } + + private static (JsonNode Parent, string LastSegment) NavigateToParent(JsonNode root, string path) + { + // path format: /a/b/c + var segs = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segs.Length == 0) throw new InvalidOperationException("Invalid path"); + + JsonNode current = root; + for (var i = 0; i < segs.Length - 1; i++) + { + var seg = segs[i]; + if (current is JsonObject obj) + { + if (!obj.TryGetPropertyValue(seg, out var next) || next is null) + { + next = new JsonObject(); + obj[seg] = next; + } + current = next; + continue; + } + + throw new InvalidOperationException($"Cannot navigate into non-object node at '{seg}'."); + } + + return (current, segs[^1]); + } + + private static bool JsonEquals(JsonNode? a, JsonNode? b) + { + if (ReferenceEquals(a, b)) return true; + if (a is null || b is null) return false; + + var aj = a.ToJsonString(AppJsonOptions.Default); + var bj = b.ToJsonString(AppJsonOptions.Default); + return string.Equals(aj, bj, StringComparison.Ordinal); + } + + private static string Join(string path, string seg) => string.IsNullOrEmpty(path) ? "/" + seg : path + "/" + seg; +} + +/// +/// Shared JsonSerializerOptions (AOT friendly). +/// +public static class AppJsonOptions +{ + public static readonly JsonSerializerOptions Default = new(System.Text.Json.JsonSerializerDefaults.Web) + { + PropertyNameCaseInsensitive = true, + WriteIndented = false, + TypeInfoResolverChain = { SlimFaasMcpGateway.Serialization.ApiJsonContext.Default } + }; + + public static readonly JsonSerializerOptions DefaultIndented = new(System.Text.Json.JsonSerializerDefaults.Web) + { + PropertyNameCaseInsensitive = true, + WriteIndented = true, + TypeInfoResolverChain = { SlimFaasMcpGateway.Serialization.ApiJsonContext.Default } + }; +} diff --git a/src/SlimFaasMcpGateway/Audit/TextDiff.cs b/src/SlimFaasMcpGateway/Audit/TextDiff.cs new file mode 100644 index 000000000..07bbf9800 --- /dev/null +++ b/src/SlimFaasMcpGateway/Audit/TextDiff.cs @@ -0,0 +1,99 @@ +using System.Text; +using System.Text.Json.Serialization; +using DiffPlex; +using DiffPlex.DiffBuilder; +using DiffPlex.DiffBuilder.Model; + +namespace SlimFaasMcpGateway.Audit; + +/// +/// Text-based diff using DiffPlex for git-like diff output +/// +public static class TextDiff +{ + public sealed record DiffLine( + [property: JsonPropertyName("type")] DiffLineType Type, + [property: JsonPropertyName("text")] string Text, + [property: JsonPropertyName("position")] int? Position = null + ); + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum DiffLineType + { + Unchanged, + Inserted, + Deleted, + Modified, + Imaginary + } + + public sealed record UnifiedDiff( + [property: JsonPropertyName("lines")] IReadOnlyList Lines + ); + + /// + /// Creates a unified diff between two text strings + /// + public static UnifiedDiff Create(string oldText, string newText) + { + var differ = new Differ(); + var builder = new InlineDiffBuilder(differ); + var diff = builder.BuildDiffModel(oldText, newText); + + var lines = new List(); + var position = 1; + + foreach (var line in diff.Lines) + { + var type = line.Type switch + { + ChangeType.Unchanged => DiffLineType.Unchanged, + ChangeType.Inserted => DiffLineType.Inserted, + ChangeType.Deleted => DiffLineType.Deleted, + ChangeType.Modified => DiffLineType.Modified, + ChangeType.Imaginary => DiffLineType.Imaginary, + _ => DiffLineType.Unchanged + }; + + // Skip imaginary lines for cleaner output + if (type == DiffLineType.Imaginary) + continue; + + lines.Add(new DiffLine(type, line.Text, type != DiffLineType.Deleted ? position : null)); + + if (type != DiffLineType.Deleted) + position++; + } + + return new UnifiedDiff(lines); + } + + /// + /// Formats a diff as a traditional unified diff string + /// + public static string Format(UnifiedDiff diff, string? fromLabel = null, string? toLabel = null) + { + var sb = new StringBuilder(); + + if (!string.IsNullOrEmpty(fromLabel) && !string.IsNullOrEmpty(toLabel)) + { + sb.AppendLine($"--- {fromLabel}"); + sb.AppendLine($"+++ {toLabel}"); + } + + foreach (var line in diff.Lines) + { + var prefix = line.Type switch + { + DiffLineType.Inserted => '+', + DiffLineType.Deleted => '-', + DiffLineType.Modified => '!', + _ => ' ' + }; + + sb.AppendLine($"{prefix}{line.Text}"); + } + + return sb.ToString(); + } +} diff --git a/src/SlimFaasMcpGateway/Auth/AesGcmSecretProtector.cs b/src/SlimFaasMcpGateway/Auth/AesGcmSecretProtector.cs new file mode 100644 index 000000000..4dd84bd57 --- /dev/null +++ b/src/SlimFaasMcpGateway/Auth/AesGcmSecretProtector.cs @@ -0,0 +1,61 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Options; +using SlimFaasMcpGateway.Options; +using SlimFaasMcpGateway.Api.Validation; + +namespace SlimFaasMcpGateway.Auth; + +public interface ISecretProtector +{ + string Protect(string plaintext); + string Unprotect(string protectedValue); +} + +public sealed class AesGcmSecretProtector : ISecretProtector +{ + private readonly byte[] _key; + + public AesGcmSecretProtector(IOptions options) + { + var keyB64 = options.Value.DiscoveryTokenEncryptionKey; + if (string.IsNullOrWhiteSpace(keyB64)) + throw new ApiException(500, "Security:DiscoveryTokenEncryptionKey is missing."); + + _key = Convert.FromBase64String(keyB64); + if (_key.Length != 32) + throw new ApiException(500, "Security:DiscoveryTokenEncryptionKey must be 32 bytes (base64)."); + } + + public string Protect(string plaintext) + { + if (plaintext is null) throw new ArgumentNullException(nameof(plaintext)); + var nonce = RandomNumberGenerator.GetBytes(12); + var pt = Encoding.UTF8.GetBytes(plaintext); + var ct = new byte[pt.Length]; + var tag = new byte[16]; + + using var aes = new AesGcm(_key); + aes.Encrypt(nonce, pt, ct, tag); + + // format: base64(nonce).base64(tag).base64(cipher) + return $"{Convert.ToBase64String(nonce)}.{Convert.ToBase64String(tag)}.{Convert.ToBase64String(ct)}"; + } + + public string Unprotect(string protectedValue) + { + if (protectedValue is null) throw new ArgumentNullException(nameof(protectedValue)); + var parts = protectedValue.Split('.', 3); + if (parts.Length != 3) throw new ApiException(500, "Invalid protected secret format."); + + var nonce = Convert.FromBase64String(parts[0]); + var tag = Convert.FromBase64String(parts[1]); + var ct = Convert.FromBase64String(parts[2]); + var pt = new byte[ct.Length]; + + using var aes = new AesGcm(_key); + aes.Decrypt(nonce, ct, tag, pt); + + return Encoding.UTF8.GetString(pt); + } +} diff --git a/src/SlimFaasMcpGateway/Auth/AuthPolicy.cs b/src/SlimFaasMcpGateway/Auth/AuthPolicy.cs new file mode 100644 index 000000000..afc127bf6 --- /dev/null +++ b/src/SlimFaasMcpGateway/Auth/AuthPolicy.cs @@ -0,0 +1,104 @@ +using SlimFaasMcpGateway.Api.Validation; + +namespace SlimFaasMcpGateway.Auth; + +public sealed class AuthPolicy +{ + public List Issuers { get; init; } = new(); + public List Audiences { get; init; } = new(); + public string? JwksUrl { get; init; } + public List Algorithms { get; init; } = new() { "RS256" }; + public Dictionary RequiredClaims { get; init; } = new(StringComparer.OrdinalIgnoreCase); + public int ClockSkewSeconds { get; init; } = 60; + public DpopPolicy Dpop { get; init; } = new(); +} + +public sealed class DpopPolicy +{ + public bool Enabled { get; init; } = false; + public int IatWindowSeconds { get; init; } = 300; + public bool RequireNonce { get; init; } = false; +} + +public static class AuthPolicyParser +{ + public static AuthPolicy Parse(string yaml) + { + try + { + var root = SimpleYaml.AsMapping(SimpleYaml.Parse(yaml)); + + var issuers = ReadStringList(root, "issuer"); + var audiences = ReadStringList(root, "audience"); + var jwksUrl = SimpleYaml.TryGetString(root, "jwksUrl") ?? SimpleYaml.TryGetString(root, "jwks_url"); + + var algos = ReadStringList(root, "algorithms"); + if (algos.Count == 0) algos.Add("RS256"); + + var clockSkew = SimpleYaml.TryGetInt(root, "clockSkewSeconds") ?? 60; + + var requiredClaims = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (SimpleYaml.TryGet(root, "requiredClaims") is SimpleYaml.Mapping rcMap) + { + foreach (var kv in rcMap.Values) + { + if (kv.Value is SimpleYaml.Scalar s && s.Value is not null) + requiredClaims[kv.Key] = s.Value.ToString()!; + } + } + + var dpop = new DpopPolicy(); + if (SimpleYaml.TryGet(root, "dpop") is SimpleYaml.Mapping dpopMap) + { + dpop = new DpopPolicy + { + Enabled = SimpleYaml.TryGetBool(dpopMap, "enabled") ?? false, + IatWindowSeconds = SimpleYaml.TryGetInt(dpopMap, "iatWindowSeconds") ?? 300, + RequireNonce = SimpleYaml.TryGetBool(dpopMap, "requireNonce") ?? false + }; + } + + if (issuers.Count == 0) + throw new ApiException(400, "Auth policy YAML: 'issuer' is required (list)."); + + if (audiences.Count == 0) + throw new ApiException(400, "Auth policy YAML: 'audience' is required (list)."); + + if (string.IsNullOrWhiteSpace(jwksUrl)) + throw new ApiException(400, "Auth policy YAML: 'jwksUrl' is required."); + + InputValidators.ValidateAbsoluteHttpUrl(jwksUrl!, "Auth policy jwksUrl"); + + return new AuthPolicy + { + Issuers = issuers, + Audiences = audiences, + JwksUrl = jwksUrl, + Algorithms = algos, + RequiredClaims = requiredClaims, + ClockSkewSeconds = clockSkew, + Dpop = dpop + }; + } + catch (ApiException) { throw; } + catch (Exception ex) + { + throw new ApiException(400, $"Auth policy YAML is invalid: {ex.Message}"); + } + } + + private static List ReadStringList(SimpleYaml.Mapping root, string key) + { + var list = new List(); + var node = SimpleYaml.TryGet(root, key); + if (node is SimpleYaml.Sequence seq) + { + foreach (var it in seq.Items) + { + if (it is SimpleYaml.Scalar s && s.Value is not null) + list.Add(s.Value.ToString()!.Trim()); + } + } + return list.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + } +} diff --git a/src/SlimFaasMcpGateway/Auth/DpopValidator.cs b/src/SlimFaasMcpGateway/Auth/DpopValidator.cs new file mode 100644 index 000000000..f531e41d5 --- /dev/null +++ b/src/SlimFaasMcpGateway/Auth/DpopValidator.cs @@ -0,0 +1,193 @@ + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.IdentityModel.Tokens; +using SlimFaasMcpGateway.Api.Validation; + +namespace SlimFaasMcpGateway.Auth; + +public interface IDpopValidator +{ + void Validate(HttpContext ctx, AuthPolicy policy, JwtSecurityToken accessToken); +} + +public sealed class DpopValidator : IDpopValidator +{ + public void Validate(HttpContext ctx, AuthPolicy policy, JwtSecurityToken accessToken) + { + if (!policy.Dpop.Enabled) + return; + + if (!ctx.Request.Headers.TryGetValue("DPoP", out var dpopHeader) || string.IsNullOrWhiteSpace(dpopHeader)) + throw new ApiException(401, "Missing DPoP proof."); + + var proof = dpopHeader.ToString(); + var handler = new JwtSecurityTokenHandler(); + + JwtSecurityToken dpopJwt; + try + { + dpopJwt = handler.ReadJwtToken(proof); + } + catch (Exception ex) + { + throw new ApiException(401, $"Invalid DPoP proof: {ex.Message}"); + } + + // Get JWK from header + if (!dpopJwt.Header.TryGetValue("jwk", out var jwkObj) || jwkObj is null) + throw new ApiException(401, "DPoP proof missing 'jwk' header."); + + var jwkJson = JsonSerializer.Serialize(jwkObj); + var jwk = new JsonWebKey(jwkJson); + + // Validate signature (issuer/audience/lifetime are not used for DPoP proof) + try + { + _ = handler.ValidateToken(proof, new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + RequireSignedTokens = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = jwk + }, out _); + } + catch (SecurityTokenException ex) + { + throw new ApiException(401, $"Invalid DPoP signature: {ex.Message}"); + } + + var htm = dpopJwt.Claims.FirstOrDefault(c => c.Type == "htm")?.Value; + var htu = dpopJwt.Claims.FirstOrDefault(c => c.Type == "htu")?.Value; + var iatStr = dpopJwt.Claims.FirstOrDefault(c => c.Type == "iat")?.Value; + var nonce = dpopJwt.Claims.FirstOrDefault(c => c.Type == "nonce")?.Value; + + if (string.IsNullOrWhiteSpace(htm) || string.IsNullOrWhiteSpace(htu) || string.IsNullOrWhiteSpace(iatStr)) + throw new ApiException(401, "DPoP proof missing required claims (htm, htu, iat)."); + + if (!string.Equals(htm, ctx.Request.Method, StringComparison.OrdinalIgnoreCase)) + throw new ApiException(401, "DPoP 'htm' does not match request method."); + + var expectedHtu = BuildAbsoluteRequestUri(ctx); + if (!UriEqualsLoose(htu, expectedHtu)) + throw new ApiException(401, "DPoP 'htu' does not match request URL."); + + if (!long.TryParse(iatStr, out var iat)) + throw new ApiException(401, "DPoP 'iat' is invalid."); + + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var window = Math.Max(1, policy.Dpop.IatWindowSeconds); + if (iat > now + 60 || now - iat > window) + throw new ApiException(401, "DPoP proof is outside allowed iat window."); + + if (policy.Dpop.RequireNonce && string.IsNullOrWhiteSpace(nonce)) + throw new ApiException(401, "DPoP nonce is required by policy."); + + // Validate binding to access token via cnf.jkt + var cnf = accessToken.Claims.FirstOrDefault(c => c.Type == "cnf")?.Value; + if (string.IsNullOrWhiteSpace(cnf)) + throw new ApiException(401, "Access token missing cnf claim required for DPoP binding."); + + var jkt = ExtractJkt(cnf); + if (string.IsNullOrWhiteSpace(jkt)) + throw new ApiException(401, "Access token cnf claim missing jkt."); + + var proofJkt = ComputeJwkThumbprint(jwk); + if (!string.Equals(jkt, proofJkt, StringComparison.Ordinal)) + throw new ApiException(401, "DPoP proof key is not bound to access token (cnf.jkt mismatch)."); + } + + private static string BuildAbsoluteRequestUri(HttpContext ctx) + { + var req = ctx.Request; + var scheme = req.Scheme; + var host = req.Host.ToUriComponent(); + var path = req.Path.ToUriComponent(); + var query = req.QueryString.HasValue ? req.QueryString.Value : ""; + return $"{scheme}://{host}{path}{query}"; + } + + private static bool UriEqualsLoose(string a, string b) + { + if (!Uri.TryCreate(a, UriKind.Absolute, out var ua)) return false; + if (!Uri.TryCreate(b, UriKind.Absolute, out var ub)) return false; + + // Compare scheme, host, path, query (case-insensitive host) + if (!string.Equals(ua.Scheme, ub.Scheme, StringComparison.OrdinalIgnoreCase)) return false; + if (!string.Equals(ua.Host, ub.Host, StringComparison.OrdinalIgnoreCase)) return false; + + var ap = ua.AbsolutePath.TrimEnd('/'); + var bp = ub.AbsolutePath.TrimEnd('/'); + if (!string.Equals(ap, bp, StringComparison.Ordinal)) return false; + + // Query must match exactly + if (!string.Equals(ua.Query ?? "", ub.Query ?? "", StringComparison.Ordinal)) return false; + + // Ports: treat default ports as equal + var pa = ua.IsDefaultPort ? DefaultPort(ua.Scheme) : ua.Port; + var pb = ub.IsDefaultPort ? DefaultPort(ub.Scheme) : ub.Port; + return pa == pb; + } + + private static int DefaultPort(string scheme) => scheme.ToLowerInvariant() switch + { + "https" => 443, + "http" => 80, + _ => -1 + }; + + private static string? ExtractJkt(string cnfClaimValue) + { + try + { + using var doc = JsonDocument.Parse(cnfClaimValue); + if (doc.RootElement.ValueKind == JsonValueKind.Object && + doc.RootElement.TryGetProperty("jkt", out var jktProp) && + jktProp.ValueKind == JsonValueKind.String) + return jktProp.GetString(); + } + catch + { + // Some providers put cnf as a nested JSON string in a claim - try to unescape + try + { + var unescaped = cnfClaimValue.Trim().Trim('\"'); + using var doc2 = JsonDocument.Parse(unescaped); + if (doc2.RootElement.ValueKind == JsonValueKind.Object && + doc2.RootElement.TryGetProperty("jkt", out var jktProp2) && + jktProp2.ValueKind == JsonValueKind.String) + return jktProp2.GetString(); + } + catch { } + } + + return null; + } + + // RFC 7638 thumbprint (SHA-256) for RSA / EC keys + private static string ComputeJwkThumbprint(JsonWebKey jwk) + { + string json; + if (string.Equals(jwk.Kty, "RSA", StringComparison.OrdinalIgnoreCase)) + { + json = $"{{\"e\":\"{jwk.E}\",\"kty\":\"RSA\",\"n\":\"{jwk.N}\"}}"; + } + else if (string.Equals(jwk.Kty, "EC", StringComparison.OrdinalIgnoreCase)) + { + json = $"{{\"crv\":\"{jwk.Crv}\",\"kty\":\"EC\",\"x\":\"{jwk.X}\",\"y\":\"{jwk.Y}\"}}"; + } + else + { + throw new ApiException(401, $"Unsupported DPoP JWK kty '{jwk.Kty}'."); + } + + var bytes = Encoding.UTF8.GetBytes(json); + var hash = SHA256.HashData(bytes); + return Base64UrlEncoder.Encode(hash); + } +} diff --git a/src/SlimFaasMcpGateway/Auth/JwtValidator.cs b/src/SlimFaasMcpGateway/Auth/JwtValidator.cs new file mode 100644 index 000000000..d0ce101dc --- /dev/null +++ b/src/SlimFaasMcpGateway/Auth/JwtValidator.cs @@ -0,0 +1,122 @@ + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.IdentityModel.Tokens; +using SlimFaasMcpGateway.Api.Validation; + +namespace SlimFaasMcpGateway.Auth; + +public sealed record JwtValidationResult(ClaimsPrincipal Principal, string? Subject, string? ClientId, JwtSecurityToken ParsedToken); + +public interface IJwtValidator +{ + Task ValidateAsync(string bearerToken, AuthPolicy policy, CancellationToken ct); +} + +public sealed class JwtValidator : IJwtValidator +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IMemoryCache _cache; + + public JwtValidator(IHttpClientFactory httpClientFactory, IMemoryCache cache) + { + _httpClientFactory = httpClientFactory; + _cache = cache; + } + + public async Task ValidateAsync(string bearerToken, AuthPolicy policy, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(bearerToken)) + throw new ApiException(401, "Missing bearer token."); + + var handler = new JwtSecurityTokenHandler(); + + var keys = await GetSigningKeysAsync(policy.JwksUrl!, ct); + + var parameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuers = policy.Issuers, + ValidateAudience = true, + ValidAudiences = policy.Audiences, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = keys, + RequireSignedTokens = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromSeconds(Math.Max(0, policy.ClockSkewSeconds)), + ValidAlgorithms = policy.Algorithms + }; + + try + { + var principal = handler.ValidateToken(bearerToken, parameters, out var validatedToken); + var jwt = validatedToken as JwtSecurityToken ?? handler.ReadJwtToken(bearerToken); + + EnforceRequiredClaims(principal, policy.RequiredClaims); + + var sub = principal.FindFirstValue("sub"); + var clientId = principal.FindFirstValue("client_id") ?? principal.FindFirstValue("azp"); + + return new JwtValidationResult(principal, sub, clientId, jwt); + } + catch (SecurityTokenException ex) + { + throw new ApiException(401, $"Invalid token: {ex.Message}"); + } + } + + private void EnforceRequiredClaims(ClaimsPrincipal principal, Dictionary required) + { + foreach (var kv in required) + { + var claim = principal.FindFirst(kv.Key); + if (claim is null) + throw new ApiException(403, $"Missing required claim '{kv.Key}'."); + + var expected = kv.Value; + if (string.IsNullOrWhiteSpace(expected) || expected == "*") + continue; + + if (string.Equals(kv.Key, "scope", StringComparison.OrdinalIgnoreCase)) + { + var scopes = (claim.Value ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (!scopes.Contains(expected, StringComparer.OrdinalIgnoreCase)) + throw new ApiException(403, $"Required scope '{expected}' not present."); + } + else + { + if (!string.Equals(claim.Value, expected, StringComparison.OrdinalIgnoreCase)) + throw new ApiException(403, $"Claim '{kv.Key}' must equal '{expected}'."); + } + } + } + + private async Task> GetSigningKeysAsync(string jwksUrl, CancellationToken ct) + { + var cacheKey = "jwks:" + jwksUrl; + if (_cache.TryGetValue(cacheKey, out IReadOnlyCollection? cached) && cached is not null) + return cached; + + var http = _httpClientFactory.CreateClient("upstream"); + var json = await http.GetStringAsync(jwksUrl, ct); + + JsonWebKeySet jwks; + try + { + jwks = new JsonWebKeySet(json); + } + catch (Exception ex) + { + throw new ApiException(502, $"Failed to parse JWKS: {ex.Message}"); + } + + var keys = jwks.Keys.Cast().ToList(); + + // Cache for 10 minutes + _cache.Set(cacheKey, keys, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }); + + return keys; + } +} diff --git a/src/SlimFaasMcpGateway/Auth/RateLimitPolicy.cs b/src/SlimFaasMcpGateway/Auth/RateLimitPolicy.cs new file mode 100644 index 000000000..b8857c871 --- /dev/null +++ b/src/SlimFaasMcpGateway/Auth/RateLimitPolicy.cs @@ -0,0 +1,76 @@ +using SlimFaasMcpGateway.Api.Validation; + +namespace SlimFaasMcpGateway.Auth; + +public enum RateLimiterType +{ + FixedWindow, + SlidingWindow, + TokenBucket, + Concurrency +} + +public sealed class RateLimitPolicy +{ + public RateLimiterType Type { get; init; } = RateLimiterType.FixedWindow; + public int PermitLimit { get; init; } = 10; + public int WindowSeconds { get; init; } = 60; + public int QueueLimit { get; init; } = 0; + public string IdentityKey { get; init; } = "ip"; // subject|client_id|ip|header:X-Api-Key + public int RejectionStatusCode { get; init; } = 429; + public string RejectionMessage { get; init; } = "Too Many Requests"; +} + +public static class RateLimitPolicyParser +{ + public static RateLimitPolicy Parse(string yaml) + { + try + { + var root = SimpleYaml.AsMapping(SimpleYaml.Parse(yaml)); + var typeStr = (SimpleYaml.TryGetString(root, "type") ?? "fixedWindow").Trim(); + + var type = typeStr.ToLowerInvariant() switch + { + "fixedwindow" or "fixed_window" or "fixed" => RateLimiterType.FixedWindow, + "slidingwindow" or "sliding_window" or "sliding" => RateLimiterType.SlidingWindow, + "tokenbucket" or "token_bucket" or "token" => RateLimiterType.TokenBucket, + "concurrency" => RateLimiterType.Concurrency, + _ => throw new ApiException(400, "Rate limit policy YAML: invalid 'type'.") + }; + + var permit = SimpleYaml.TryGetInt(root, "permitLimit") ?? 10; + var windowSeconds = SimpleYaml.TryGetInt(root, "windowSeconds") ?? 60; + var queue = SimpleYaml.TryGetInt(root, "queueLimit") ?? 0; + var identity = (SimpleYaml.TryGetString(root, "identity") ?? "ip").Trim(); + var rejCode = SimpleYaml.TryGetInt(root, "rejectionStatusCode") ?? 429; + var rejMsg = (SimpleYaml.TryGetString(root, "rejectionMessage") ?? "Too Many Requests").Trim(); + + if (permit <= 0) throw new ApiException(400, "Rate limit policy YAML: permitLimit must be > 0."); + if (windowSeconds <= 0) throw new ApiException(400, "Rate limit policy YAML: windowSeconds must be > 0."); + if (queue < 0) throw new ApiException(400, "Rate limit policy YAML: queueLimit must be >= 0."); + + if (identity.StartsWith("header:", StringComparison.OrdinalIgnoreCase)) + { + var header = identity.Substring("header:".Length).Trim(); + if (string.IsNullOrWhiteSpace(header)) throw new ApiException(400, "Rate limit policy YAML: header identity requires a header name."); + } + + return new RateLimitPolicy + { + Type = type, + PermitLimit = permit, + WindowSeconds = windowSeconds, + QueueLimit = queue, + IdentityKey = identity, + RejectionStatusCode = rejCode, + RejectionMessage = rejMsg + }; + } + catch (ApiException) { throw; } + catch (Exception ex) + { + throw new ApiException(400, $"Rate limit policy YAML is invalid: {ex.Message}"); + } + } +} diff --git a/src/SlimFaasMcpGateway/ClientApp/.env.development b/src/SlimFaasMcpGateway/ClientApp/.env.development new file mode 100644 index 000000000..67ee2931e --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/.env.development @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:5106 diff --git a/src/SlimFaasMcpGateway/ClientApp/.gitignore b/src/SlimFaasMcpGateway/ClientApp/.gitignore new file mode 100644 index 000000000..f58e08162 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/.gitignore @@ -0,0 +1,17 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment files +.env.local +.env.*.local + +# Editor +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db diff --git a/src/SlimFaasMcpGateway/ClientApp/README.md b/src/SlimFaasMcpGateway/ClientApp/README.md new file mode 100644 index 000000000..198d3e354 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/README.md @@ -0,0 +1,88 @@ +# SlimFaasMcpGateway - Frontend Application + +Frontend React/Vite pour la gateway MCP. + +## Développement + +### Installation des dépendances +```bash +npm install +``` + +### Lancer en mode développement (avec hot-reload) +```bash +npm run dev +``` + +L'application sera disponible sur `http://localhost:5173` avec proxy automatique vers l'API backend sur `http://localhost:5269`. + +### Build de production +```bash +npm run build +``` + +Les fichiers compilés seront générés dans le dossier `dist/`. + +### Preview du build de production +```bash +npm run preview +``` + +## Structure + +``` +ClientApp/ +├── src/ +│ ├── main.tsx # Point d'entrée +│ ├── lib/ +│ │ ├── api.ts # Client API +│ │ └── types.ts # Types TypeScript +│ ├── pages/ +│ │ ├── App.tsx # Page principale +│ │ ├── ConfigurationsPage.tsx # Liste des configurations +│ │ ├── ConfigurationEditorPage.tsx # Éditeur de configuration +│ │ ├── DeploymentPage.tsx # Gestion des déploiements +│ │ └── TenantsPage.tsx # Gestion des tenants +│ └── styles/ +│ ├── index.scss # Styles globaux +│ ├── components.scss # Styles des composants +│ └── tokens.scss # Variables de design +├── index.html +├── package.json +├── tsconfig.json +└── vite.config.ts +``` + +## Configuration + +### Proxy API (vite.config.ts) + +En mode développement, les requêtes vers `/api`, `/gateway`, `/health` et `/metrics` sont automatiquement proxifiées vers le backend sur `http://localhost:5269`. + +### Variables d'environnement + +Créez un fichier `.env.local` pour les variables locales : + +```env +# URL de l'API (utilisé uniquement en production) +VITE_API_URL=http://localhost:5269 +``` + +## Build avec .NET + +Le frontend est automatiquement compilé lors du build .NET : + +```bash +# Depuis le répertoire parent (SlimFaasMcpGateway.Api) +dotnet build +``` + +Les fichiers compilés sont copiés dans `../wwwroot/` et servis par l'API .NET. + +## Technologies + +- **React 18** - Framework UI +- **TypeScript** - Typage statique +- **Vite** - Build tool et dev server +- **React Router** - Routing côté client +- **Sass** - Préprocesseur CSS diff --git a/src/SlimFaasMcpGateway/ClientApp/index.html b/src/SlimFaasMcpGateway/ClientApp/index.html new file mode 100644 index 000000000..1e3d5f313 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/index.html @@ -0,0 +1,12 @@ + + + + + + SlimFaas MCP Gateway + + +
+ + + diff --git a/src/SlimFaasMcpGateway/ClientApp/package-lock.json b/src/SlimFaasMcpGateway/ClientApp/package-lock.json new file mode 100644 index 000000000..dbb6725a6 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/package-lock.json @@ -0,0 +1,2262 @@ +{ + "name": "slimfaas-mcp-gateway-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "slimfaas-mcp-gateway-ui", + "version": "0.1.0", + "dependencies": { + "diff": "^8.0.3", + "react": "^18.3.1", + "react-diff-view": "^3.3.2", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.2", + "sass": "^1.77.8", + "typescript": "^5.6.3", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@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-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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "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.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "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-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/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/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "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/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.278", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz", + "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/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/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/gitdiff-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz", + "integrity": "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==", + "license": "MIT" + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/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/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/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-diff-view": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/react-diff-view/-/react-diff-view-3.3.2.tgz", + "integrity": "sha512-wPVq4ktTcGOHbhnWKU/gHLtd3N2Xd+OZ/XQWcKA06dsxlSsESePAumQILwHtiak2nMCMiWcIfBpqZ5OiharUPA==", + "license": "MIT", + "dependencies": { + "classnames": "^2.3.2", + "diff-match-patch": "^1.0.5", + "gitdiff-parser": "^0.3.1", + "lodash": "^4.17.21", + "shallow-equal": "^3.1.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "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" + } + } +} diff --git a/src/SlimFaasMcpGateway/ClientApp/package.json b/src/SlimFaasMcpGateway/ClientApp/package.json new file mode 100644 index 000000000..7ee473f0d --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/package.json @@ -0,0 +1,26 @@ +{ + "name": "slimfaas-mcp-gateway-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "diff": "^8.0.3", + "react": "^18.3.1", + "react-diff-view": "^3.3.2", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.2", + "sass": "^1.77.8", + "typescript": "^5.6.3", + "vite": "^5.4.8" + } +} diff --git a/src/SlimFaasMcpGateway/ClientApp/src/components/DiffViewer.tsx b/src/SlimFaasMcpGateway/ClientApp/src/components/DiffViewer.tsx new file mode 100644 index 000000000..bdb63fd59 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/src/components/DiffViewer.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import "../styles/diff.scss"; + +interface DiffLine { + type: "Unchanged" | "Inserted" | "Deleted" | "Modified" | "Imaginary"; + text: string; + position: number | null; +} + +interface DiffViewerProps { + lines: DiffLine[]; +} + +export default function DiffViewer({ lines }: DiffViewerProps) { + if (!lines || lines.length === 0) { + return
No changes
; + } + + return ( +
+ + + {lines.map((line, index) => { + const className = `diff-viewer__line diff-viewer__line--${line.type.toLowerCase()}`; + const prefix = getPrefix(line.type); + + return ( + + + + + + ); + })} + +
{line.position ?? ""}{prefix} + {line.text} +
+
+ ); +} + +function getPrefix(type: string): string { + switch (type) { + case "Inserted": + return "+"; + case "Deleted": + return "-"; + case "Modified": + return "~"; + default: + return " "; + } +} diff --git a/src/SlimFaasMcpGateway/ClientApp/src/lib/api.ts b/src/SlimFaasMcpGateway/ClientApp/src/lib/api.ts new file mode 100644 index 000000000..1518becb4 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/src/lib/api.ts @@ -0,0 +1,41 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? ""; + +export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + +export async function apiJson( + path: string, + method: HttpMethod = "GET", + body?: unknown, + auditAuthor?: string +): Promise { + const res = await fetch(`${API_BASE_URL}${path}`, { + method, + headers: { + "Content-Type": "application/json", + ...(auditAuthor ? { "X-Audit-Author": auditAuthor } : {}), + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + + const text = await res.text(); + if (!res.ok) { + try { + const parsed = JSON.parse(text); + throw new Error(parsed?.error ?? text); + } catch { + throw new Error(text || `${res.status} ${res.statusText}`); + } + } + + return text ? (JSON.parse(text) as T) : (undefined as unknown as T); +} + +export async function apiText( + path: string, + method: HttpMethod = "GET" +): Promise { + const res = await fetch(`${API_BASE_URL}${path}`, { method }); + const text = await res.text(); + if (!res.ok) throw new Error(text || `${res.status} ${res.statusText}`); + return text; +} diff --git a/src/SlimFaasMcpGateway/ClientApp/src/lib/types.ts b/src/SlimFaasMcpGateway/ClientApp/src/lib/types.ts new file mode 100644 index 000000000..854ee0d01 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/src/lib/types.ts @@ -0,0 +1,73 @@ +export type TenantListItemDto = { + id: string; + name: string; + description?: string | null; +}; + +export type ConfigurationListItemDto = { + id: string; + tenantName: string; + name: string; + gatewayUrl: string; + createdAtUtc: string; + defaultDeploymentEnvironment: string; +}; + +export type ConfigurationDto = { + id: string; + tenantId?: string | null; + tenantName: string; + name: string; + upstreamMcpUrl: string; + description?: string | null; + hasDiscoveryJwtToken: boolean; + catalogOverrideYaml?: string | null; + enforceAuthEnabled: boolean; + authPolicyYaml?: string | null; + rateLimitEnabled: boolean; + rateLimitPolicyYaml?: string | null; + catalogCacheTtlMinutes: number; + version: number; + createdAtUtc: string; + updatedAtUtc: string; +}; + +export type AuditHistoryItemDto = { index: number; modifiedAtUtc: number; author: string }; + +export type DeploymentStateDto = { environmentName: string; deployedAuditIndex?: number | null }; + +export type DeploymentOverviewDto = { + configurationId: string; + tenantName: string; + configurationName: string; + environments: DeploymentStateDto[]; + history: AuditHistoryItemDto[]; +}; + +export type AuditDiffDto = { + from: { index: number; modifiedAtUtc: number; author: string }; + to: { index: number; modifiedAtUtc: number; author: string }; + patch: { path: string; value?: any; remove: boolean }[]; +}; + +export type DiffLineType = "Unchanged" | "Inserted" | "Deleted" | "Modified" | "Imaginary"; + +export type DiffLine = { + type: DiffLineType; + text: string; + position: number | null; +}; + +export type UnifiedDiff = { + lines: DiffLine[]; +}; + +export type AuditTextDiffDto = { + from: { index: number; modifiedAtUtc: number; author: string }; + to: { index: number; modifiedAtUtc: number; author: string }; + unifiedDiff: UnifiedDiff; +}; + +export type EnvironmentListDto = { environments: string[] }; + +export type LoadCatalogResponseDto = { catalogYaml: string }; diff --git a/src/SlimFaasMcpGateway/ClientApp/src/main.tsx b/src/SlimFaasMcpGateway/ClientApp/src/main.tsx new file mode 100644 index 000000000..bf72797c4 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/src/main.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./pages/App"; +import "./styles/index.scss"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/src/SlimFaasMcpGateway/ClientApp/src/pages/App.tsx b/src/SlimFaasMcpGateway/ClientApp/src/pages/App.tsx new file mode 100644 index 000000000..00681cf96 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/src/pages/App.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { NavLink, Route, Routes } from "react-router-dom"; +import ConfigurationsPage from "./ConfigurationsPage"; +import ConfigurationEditorPage from "./ConfigurationEditorPage"; +import DeploymentPage from "./DeploymentPage"; +import TenantsPage from "./TenantsPage"; + +export default function App() { + return ( +
+
+
+
+ +
+ +
+ + } /> + } /> + } /> + } /> + } /> + +
+ +
+ Minimal MCP gateway UI — edit configs, deploy by environment, proxy upstream MCP servers. +
+
+ ); +} diff --git a/src/SlimFaasMcpGateway/ClientApp/src/pages/ConfigurationEditorPage.tsx b/src/SlimFaasMcpGateway/ClientApp/src/pages/ConfigurationEditorPage.tsx new file mode 100644 index 000000000..a90747e23 --- /dev/null +++ b/src/SlimFaasMcpGateway/ClientApp/src/pages/ConfigurationEditorPage.tsx @@ -0,0 +1,361 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { apiJson } from "../lib/api"; +import type { ConfigurationDto, EnvironmentListDto, LoadCatalogResponseDto, TenantListItemDto } from "../lib/types"; + +type Mode = "create" | "edit"; + +const DEFAULT_AUTH_POLICY = `issuer: + - "https://issuer.example.com" +audience: + - "mcp-gateway" +jwksUrl: "https://issuer.example.com/.well-known/jwks.json" +algorithms: + - "RS256" +requiredClaims: + scope: "mcp" +dpop: + enabled: false + iatWindowSeconds: 300 +`; + +const DEFAULT_RATE_LIMIT_POLICY = `type: fixedWindow +permitLimit: 30 +windowSeconds: 60 +queueLimit: 0 +identity: subject +rejectionStatusCode: 429 +rejectionMessage: "Too Many Requests" +`; + +export default function ConfigurationEditorPage({ mode }: { mode: Mode }) { + const { id } = useParams(); + const navigate = useNavigate(); + + const [tenants, setTenants] = useState([]); + const [envs, setEnvs] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [author, setAuthor] = useState("unknown"); + + const [tenantId, setTenantId] = useState(null); + const [name, setName] = useState(""); + const [upstreamMcpUrl, setUpstreamMcpUrl] = useState(""); + const [description, setDescription] = useState(""); + const [discoveryJwtToken, setDiscoveryJwtToken] = useState(""); // empty means clear, undefined means keep; UI uses checkbox + const [changeDiscoveryToken, setChangeDiscoveryToken] = useState(false); + + const [catalogOverrideYaml, setCatalogOverrideYaml] = useState(""); + const [enforceAuthEnabled, setEnforceAuthEnabled] = useState(false); + const [authPolicyYaml, setAuthPolicyYaml] = useState(DEFAULT_AUTH_POLICY); + const [rateLimitEnabled, setRateLimitEnabled] = useState(false); + const [rateLimitPolicyYaml, setRateLimitPolicyYaml] = useState(DEFAULT_RATE_LIMIT_POLICY); + const [catalogCacheTtlMinutes, setCatalogCacheTtlMinutes] = useState(5); + + const [hasExistingToken, setHasExistingToken] = useState(false); + + const pageTitle = mode === "create" ? "New configuration" : "Edit configuration"; + + async function loadLookups() { + const [t, e] = await Promise.all([ + apiJson("/api/tenants"), + apiJson("/api/environments"), + ]); + setTenants(t); + setEnvs(e.environments); + } + + async function loadConfiguration(cfgId: string) { + const dto = await apiJson(`/api/configurations/${cfgId}`); + setTenantId(dto.tenantId ?? null); + setName(dto.name); + setUpstreamMcpUrl(dto.upstreamMcpUrl); + setDescription(dto.description ?? ""); + setCatalogOverrideYaml(dto.catalogOverrideYaml ?? ""); + setEnforceAuthEnabled(dto.enforceAuthEnabled); + setAuthPolicyYaml(dto.authPolicyYaml ?? DEFAULT_AUTH_POLICY); + setRateLimitEnabled(dto.rateLimitEnabled); + setRateLimitPolicyYaml(dto.rateLimitPolicyYaml ?? DEFAULT_RATE_LIMIT_POLICY); + setCatalogCacheTtlMinutes(dto.catalogCacheTtlMinutes ?? 0); + setHasExistingToken(dto.hasDiscoveryJwtToken); + } + + useEffect(() => { + void (async () => { + try { + setError(null); + await loadLookups(); + if (mode === "edit" && id) { + await loadConfiguration(id); + } else { + // defaults + setTenantId(null); + } + } catch (e: any) { + setError(e?.message ?? String(e)); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode, id]); + + const tenantOptions = useMemo(() => { + const arr = [...tenants]; + arr.sort((a, b) => a.name.localeCompare(b.name)); + return arr; + }, [tenants]); + + async function loadCatalog() { + if (!id) return; + try { + setLoading(true); + setError(null); + const res = await apiJson(`/api/configurations/${id}/load-catalog`, "POST"); + setCatalogOverrideYaml(res.catalogYaml); + } catch (e: any) { + setError(e?.message ?? String(e)); + } finally { + setLoading(false); + } + } + + async function save() { + try { + setLoading(true); + setError(null); + + const payload = { + name, + tenantId: tenantId || null, + upstreamMcpUrl, + description: description || null, + discoveryJwtToken: changeDiscoveryToken ? discoveryJwtToken : null, + catalogOverrideYaml: catalogOverrideYaml || null, + enforceAuthEnabled, + authPolicyYaml: enforceAuthEnabled ? authPolicyYaml : null, + rateLimitEnabled, + rateLimitPolicyYaml: rateLimitEnabled ? rateLimitPolicyYaml : null, + catalogCacheTtlMinutes: Number.isFinite(catalogCacheTtlMinutes) ? catalogCacheTtlMinutes : 0, + }; + + if (mode === "create") { + const created = await apiJson("/api/configurations", "POST", payload, author); + navigate(`/configurations/${created.id}`); + } else if (mode === "edit" && id) { + await apiJson(`/api/configurations/${id}`, "PUT", payload, author); + await loadConfiguration(id); + } + } catch (e: any) { + setError(e?.message ?? String(e)); + } finally { + setLoading(false); + } + } + + async function remove() { + if (!id) return; + if (!confirm("Delete this configuration?")) return; + try { + setLoading(true); + setError(null); + await apiJson(`/api/configurations/${id}`, "DELETE", undefined, author); + navigate("/"); + } catch (e: any) { + setError(e?.message ?? String(e)); + } finally { + setLoading(false); + } + } + + return ( +
+
+
+

{pageTitle}

+ {mode === "edit" && id && ( +
+ Deployment + + Environments: {envs.join(", ")} +
+ )} +
+
+ Back + {mode === "edit" && id && ( + + )} + +
+
+ + {error &&
{error}
} + +
+
+

Basics

+ + + + + + + + + + + + + +
+ +
+ setChangeDiscoveryToken(e.target.checked)} + /> + +
+ + {changeDiscoveryToken && ( + + )} + + {mode === "edit" && id && ( +
+ +
+ )} +
+ +
+

Catalog override (YAML)

+