diff --git a/Svrnty.CQRS.Altcha.Abstractions/IAltchaDifficultyAdvisor.cs b/Svrnty.CQRS.Altcha.Abstractions/IAltchaDifficultyAdvisor.cs
new file mode 100644
index 0000000..e484a65
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/IAltchaDifficultyAdvisor.cs
@@ -0,0 +1,27 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Resolves a per-request PoW complexity (search-space upper bound) for the
+/// next Altcha challenge the server will mint. Implementations may consult
+/// per-actor signals — repeat-offender counters, threat-intel headers,
+/// reputation scores — to scale difficulty up for suspicious actors while
+/// keeping the baseline cheap for everyone else.
+///
+///
+/// The framework ships a no-op that
+/// always returns null, meaning "use the upstream service's configured
+/// default complexity." Applications opt into adaptive difficulty by
+/// replacing the registration with their own implementation; consult request
+/// context via
+/// or scoped DI.
+///
+public interface IAltchaDifficultyAdvisor
+{
+ ///
+ /// Returns the desired maxNumber (PoW search-space upper bound)
+ /// for the next challenge, or null to defer to the upstream
+ /// service default. The Altcha server clamps to its configured min/max,
+ /// so callers don't need to enforce bounds here.
+ ///
+ Task GetComplexityAsync(CancellationToken cancellationToken = default);
+}
diff --git a/Svrnty.CQRS.Altcha.Grpc/AltchaGrpcChallengeProvider.cs b/Svrnty.CQRS.Altcha.Grpc/AltchaGrpcChallengeProvider.cs
index 797a2c2..4ecf35f 100644
--- a/Svrnty.CQRS.Altcha.Grpc/AltchaGrpcChallengeProvider.cs
+++ b/Svrnty.CQRS.Altcha.Grpc/AltchaGrpcChallengeProvider.cs
@@ -14,15 +14,18 @@ public sealed class AltchaGrpcChallengeProvider : IAltchaChallengeProvider
{
private readonly AltchaService.AltchaServiceClient _client;
private readonly IOptions _options;
+ private readonly IAltchaDifficultyAdvisor _advisor;
private readonly ILogger _logger;
public AltchaGrpcChallengeProvider(
AltchaService.AltchaServiceClient client,
IOptions options,
+ IAltchaDifficultyAdvisor advisor,
ILogger logger)
{
_client = client;
_options = options;
+ _advisor = advisor;
_logger = logger;
}
@@ -32,10 +35,18 @@ public sealed class AltchaGrpcChallengeProvider : IAltchaChallengeProvider
var metadata = await AltchaCallCredentials.BuildMetadataAsync(opts, cancellationToken);
var deadline = DateTime.UtcNow.Add(opts.CallTimeout);
+ var request = new CreateChallengeRequest();
+ var advisedComplexity = await _advisor.GetComplexityAsync(cancellationToken);
+ if (advisedComplexity is uint complexity)
+ {
+ request.Complexity = complexity;
+ _logger.LogDebug("Altcha advisor requested complexity {Complexity}", complexity);
+ }
+
try
{
var response = await _client.CreateChallengeAsync(
- new CreateChallengeRequest(),
+ request,
headers: metadata,
deadline: deadline,
cancellationToken: cancellationToken);
diff --git a/Svrnty.CQRS.Altcha/NullAltchaDifficultyAdvisor.cs b/Svrnty.CQRS.Altcha/NullAltchaDifficultyAdvisor.cs
new file mode 100644
index 0000000..ce674b3
--- /dev/null
+++ b/Svrnty.CQRS.Altcha/NullAltchaDifficultyAdvisor.cs
@@ -0,0 +1,15 @@
+using Svrnty.CQRS.Altcha.Abstractions;
+
+namespace Svrnty.CQRS.Altcha;
+
+///
+/// No-op fallback registered by AddSvrntyAltcha(). Always returns
+/// null, leaving complexity at the upstream Altcha service's
+/// configured default. Applications that want adaptive difficulty
+/// replace this registration with their own implementation.
+///
+internal sealed class NullAltchaDifficultyAdvisor : IAltchaDifficultyAdvisor
+{
+ public Task GetComplexityAsync(CancellationToken cancellationToken = default)
+ => Task.FromResult(null);
+}
diff --git a/Svrnty.CQRS.Altcha/ServiceCollectionExtensions.cs b/Svrnty.CQRS.Altcha/ServiceCollectionExtensions.cs
index 0bccdc2..35974b2 100644
--- a/Svrnty.CQRS.Altcha/ServiceCollectionExtensions.cs
+++ b/Svrnty.CQRS.Altcha/ServiceCollectionExtensions.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Svrnty.CQRS.Abstractions.Security;
+using Svrnty.CQRS.Altcha.Abstractions;
namespace Svrnty.CQRS.Altcha;
@@ -9,22 +10,27 @@ public static class ServiceCollectionExtensions
///
/// Registers as both an
/// and an
- /// . The check is a no-op until
- /// an
- /// implementation is also registered (typically via
- /// AddSvrntyAltchaGrpcVerifier(...) from
- /// Svrnty.CQRS.Altcha.Grpc).
+ /// , plus a no-op
+ /// that defers to the upstream
+ /// Altcha service's configured default complexity. Applications opt
+ /// into adaptive difficulty by registering their own
+ /// before or after this call —
+ /// the TryAdd registration here yields to any existing one.
///
///
- /// Idempotent for the concrete check; the multi-instance interface
- /// registrations are added unconditionally, so callers should invoke
- /// this exactly once per application startup.
+ /// The check is a no-op until an
+ /// implementation is also registered
+ /// (typically via AddSvrntyAltchaGrpcVerifier(...) from
+ /// Svrnty.CQRS.Altcha.Grpc). Idempotent for the concrete check;
+ /// the multi-instance interface registrations are added unconditionally,
+ /// so callers should invoke this exactly once per application startup.
///
public static IServiceCollection AddSvrntyAltcha(this IServiceCollection services)
{
services.TryAddSingleton();
services.AddSingleton(sp => sp.GetRequiredService());
services.AddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton();
return services;
}
}