diff --git a/Svrnty.CQRS.Altcha.MinimalApi/AltchaChallengeDto.cs b/Svrnty.CQRS.Altcha.MinimalApi/AltchaChallengeDto.cs
new file mode 100644
index 0000000..5fc4b8e
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.MinimalApi/AltchaChallengeDto.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+
+namespace Svrnty.CQRS.Altcha.MinimalApi;
+
+///
+/// JSON projection of
+/// in the exact shape the
+/// altcha widget v3
+/// expects from a challengeurl response. Property names are
+/// lowercased and challenge (no underscore) to match the widget.
+///
+public sealed class AltchaChallengeDto
+{
+ [JsonPropertyName("algorithm")]
+ public required string Algorithm { get; init; }
+
+ [JsonPropertyName("challenge")]
+ public required string Challenge { get; init; }
+
+ [JsonPropertyName("salt")]
+ public required string Salt { get; init; }
+
+ [JsonPropertyName("signature")]
+ public required string Signature { get; init; }
+
+ [JsonPropertyName("maxnumber")]
+ public required uint MaxNumber { get; init; }
+}
diff --git a/Svrnty.CQRS.Altcha.MinimalApi/EndpointRouteBuilderExtensions.cs b/Svrnty.CQRS.Altcha.MinimalApi/EndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000..ad120dc
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.MinimalApi/EndpointRouteBuilderExtensions.cs
@@ -0,0 +1,50 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Svrnty.CQRS.Altcha.Abstractions;
+
+namespace Svrnty.CQRS.Altcha.MinimalApi;
+
+public static class EndpointRouteBuilderExtensions
+{
+ ///
+ /// Maps GET {routePrefix} (default /api/altcha/challenge)
+ /// returning a fresh challenge in the JSON shape the
+ /// altcha widget
+ /// consumes via its challengeurl attribute.
+ ///
+ ///
+ /// Requires an to be registered
+ /// (typically by AddSvrntyAltchaGrpcVerifier(...)). The endpoint
+ /// allows anonymous access — the whole point is gating mutations from
+ /// unauthenticated callers, so the challenge endpoint must be reachable
+ /// without credentials.
+ ///
+ public static IEndpointRouteBuilder MapSvrntyAltchaChallenge(
+ this IEndpointRouteBuilder endpoints,
+ string routePrefix = "/api/altcha/challenge")
+ {
+ endpoints.MapGet(routePrefix, async (
+ IAltchaChallengeProvider provider,
+ CancellationToken cancellationToken) =>
+ {
+ var challenge = await provider.CreateAsync(cancellationToken);
+ return Results.Ok(new AltchaChallengeDto
+ {
+ Algorithm = challenge.Algorithm,
+ Challenge = challenge.Challenge,
+ Salt = challenge.Salt,
+ Signature = challenge.Signature,
+ MaxNumber = challenge.MaxNumber
+ });
+ })
+ .AllowAnonymous()
+ .WithName("Altcha_Challenge_Get")
+ .WithTags("Altcha")
+ .Produces(200)
+ .Produces(503);
+
+ return endpoints;
+ }
+}
diff --git a/Svrnty.CQRS.Altcha.MinimalApi/Svrnty.CQRS.Altcha.MinimalApi.csproj b/Svrnty.CQRS.Altcha.MinimalApi/Svrnty.CQRS.Altcha.MinimalApi.csproj
new file mode 100644
index 0000000..861ddd0
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.MinimalApi/Svrnty.CQRS.Altcha.MinimalApi.csproj
@@ -0,0 +1,37 @@
+
+
+ net10.0
+ false
+ 14
+ enable
+ enable
+
+ Svrnty
+ David Lebee, Mathias Beaulieu-Duncan
+ icon.png
+ README.md
+ https://git.openharbor.io/svrnty/dotnet-cqrs
+ git
+ true
+ MIT
+
+ portable
+ true
+ true
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Svrnty.CQRS.sln b/Svrnty.CQRS.sln
index 4b753ab..33baa37 100644
--- a/Svrnty.CQRS.sln
+++ b/Svrnty.CQRS.sln
@@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha", "Svrnt
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha.Grpc", "Svrnty.CQRS.Altcha.Grpc\Svrnty.CQRS.Altcha.Grpc.csproj", "{628DE10C-FCDB-418B-8341-FA246BBCF70E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha.MinimalApi", "Svrnty.CQRS.Altcha.MinimalApi\Svrnty.CQRS.Altcha.MinimalApi.csproj", "{26B24C13-FA06-4611-A371-2B640B8066F2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -299,6 +301,18 @@ Global
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Release|x64.Build.0 = Release|Any CPU
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Release|x86.ActiveCfg = Release|Any CPU
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Release|x86.Build.0 = Release|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Debug|x64.Build.0 = Debug|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Debug|x86.Build.0 = Debug|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Release|x64.ActiveCfg = Release|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Release|x64.Build.0 = Release|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Release|x86.ActiveCfg = Release|Any CPU
+ {26B24C13-FA06-4611-A371-2B640B8066F2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE