feat(altcha): add Svrnty.CQRS.Altcha.Grpc with default verifier + proto
The default transport for IAltchaVerifier / IAltchaChallengeProvider —
calls a self-hosted altcha service over gRPC.
Wire contract
- Protos/altcha.proto defines svrnty.cqrs.altcha.v1.AltchaService with
CreateChallenge + VerifyChallenge RPCs. Shipped in this package as
source-of-truth; Go (and other) implementations vendor a copy.
- Challenge.challenge_hash is named (not "challenge") to avoid a C#
property/class name collision; the MinimalApi widget JSON remaps.
Runtime
- AltchaGrpcVerifier maps RpcException → AltchaVerifyResult.Fail with
a diagnostic reason ("verify-timeout", "service-unavailable", etc.)
so the auth check surfaces a clean Unauthorized without leaking
transport detail.
- AltchaGrpcChallengeProvider lets create-challenge failures bubble
(challenge endpoint should 5xx if altcha is down — clients retry).
- AltchaGrpcOptions.TokenProvider hook for consumer-supplied HMAC
service-token minting (plan-b will plug in ServiceTokenIssuer).
- AddGrpcClient<AltchaServiceClient> registered with HttpClientFactory.
AddSvrntyAltchaGrpcVerifier(Action<...>) and overload binding from
IConfiguration cover both wiring styles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
69e29d4f6d
commit
4446288bb6
27
Svrnty.CQRS.Altcha.Grpc/AltchaCallCredentials.cs
Normal file
27
Svrnty.CQRS.Altcha.Grpc/AltchaCallCredentials.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
|
||||||
|
namespace Svrnty.CQRS.Altcha.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper that builds gRPC call metadata (an <c>Authorization</c> header)
|
||||||
|
/// from <see cref="AltchaGrpcOptions.TokenProvider"/>. Kept as a separate
|
||||||
|
/// shared helper so the verifier and challenge provider apply identical
|
||||||
|
/// rules.
|
||||||
|
/// </summary>
|
||||||
|
internal static class AltchaCallCredentials
|
||||||
|
{
|
||||||
|
public static async Task<Metadata?> BuildMetadataAsync(AltchaGrpcOptions options, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (options.TokenProvider is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var token = await options.TokenProvider(cancellationToken);
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new Metadata
|
||||||
|
{
|
||||||
|
{ "Authorization", $"Bearer {token}" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Svrnty.CQRS.Altcha.Grpc/AltchaGrpcChallengeProvider.cs
Normal file
58
Svrnty.CQRS.Altcha.Grpc/AltchaGrpcChallengeProvider.cs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Svrnty.CQRS.Altcha.Abstractions;
|
||||||
|
|
||||||
|
namespace Svrnty.CQRS.Altcha.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default <see cref="IAltchaChallengeProvider"/> backed by gRPC. Calls
|
||||||
|
/// <c>AltchaService.CreateChallenge</c> on the configured endpoint and
|
||||||
|
/// projects the response onto <see cref="AltchaChallenge"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AltchaGrpcChallengeProvider : IAltchaChallengeProvider
|
||||||
|
{
|
||||||
|
private readonly AltchaService.AltchaServiceClient _client;
|
||||||
|
private readonly IOptions<AltchaGrpcOptions> _options;
|
||||||
|
private readonly ILogger<AltchaGrpcChallengeProvider> _logger;
|
||||||
|
|
||||||
|
public AltchaGrpcChallengeProvider(
|
||||||
|
AltchaService.AltchaServiceClient client,
|
||||||
|
IOptions<AltchaGrpcOptions> options,
|
||||||
|
ILogger<AltchaGrpcChallengeProvider> logger)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_options = options;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AltchaChallenge> CreateAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var opts = _options.Value;
|
||||||
|
var metadata = await AltchaCallCredentials.BuildMetadataAsync(opts, cancellationToken);
|
||||||
|
var deadline = DateTime.UtcNow.Add(opts.CallTimeout);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _client.CreateChallengeAsync(
|
||||||
|
new CreateChallengeRequest(),
|
||||||
|
headers: metadata,
|
||||||
|
deadline: deadline,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return new AltchaChallenge
|
||||||
|
{
|
||||||
|
Algorithm = response.Algorithm,
|
||||||
|
Challenge = response.ChallengeHash,
|
||||||
|
Salt = response.Salt,
|
||||||
|
Signature = response.Signature,
|
||||||
|
MaxNumber = response.Maxnumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (RpcException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Altcha create-challenge failed against {Endpoint}: {Status}", opts.Endpoint, ex.StatusCode);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Svrnty.CQRS.Altcha.Grpc/AltchaGrpcOptions.cs
Normal file
31
Svrnty.CQRS.Altcha.Grpc/AltchaGrpcOptions.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
namespace Svrnty.CQRS.Altcha.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for <see cref="AltchaGrpcVerifier"/> and
|
||||||
|
/// <see cref="AltchaGrpcChallengeProvider"/>. Bind from configuration
|
||||||
|
/// (e.g. <c>"Altcha"</c> section) or pass via the registration delegate.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AltchaGrpcOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// gRPC endpoint of the altcha service. Typically the internal
|
||||||
|
/// docker / k8s address — e.g. <c>http://altcha:9090</c> or
|
||||||
|
/// <c>https://altcha.planb.svc.cluster.local:9090</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string Endpoint { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional per-call HMAC service-token provider. When set, the
|
||||||
|
/// returned string is sent as <c>Authorization: Bearer <token></c>
|
||||||
|
/// on every outbound gRPC call. Use this to integrate with whatever
|
||||||
|
/// service-auth scheme the rest of the deployment uses (e.g. plan-b's
|
||||||
|
/// <c>ServiceTokenIssuer.GetToken("altcha")</c>).
|
||||||
|
/// </summary>
|
||||||
|
public Func<CancellationToken, Task<string>>? TokenProvider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-call timeout for both <c>CreateChallenge</c> and
|
||||||
|
/// <c>VerifyChallenge</c>. Defaults to 5s.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan CallTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
}
|
||||||
66
Svrnty.CQRS.Altcha.Grpc/AltchaGrpcVerifier.cs
Normal file
66
Svrnty.CQRS.Altcha.Grpc/AltchaGrpcVerifier.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Svrnty.CQRS.Altcha.Abstractions;
|
||||||
|
|
||||||
|
namespace Svrnty.CQRS.Altcha.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default <see cref="IAltchaVerifier"/> backed by gRPC. Calls
|
||||||
|
/// <c>AltchaService.VerifyChallenge</c> on the configured endpoint and
|
||||||
|
/// maps any failure (transport error, deadline, server-reported failure)
|
||||||
|
/// to <see cref="AltchaVerifyResult.Fail"/>. Verification failures are
|
||||||
|
/// safe defaults — callers see an <c>Unauthorized</c> outcome from the
|
||||||
|
/// auth check.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AltchaGrpcVerifier : IAltchaVerifier
|
||||||
|
{
|
||||||
|
private readonly AltchaService.AltchaServiceClient _client;
|
||||||
|
private readonly IOptions<AltchaGrpcOptions> _options;
|
||||||
|
private readonly ILogger<AltchaGrpcVerifier> _logger;
|
||||||
|
|
||||||
|
public AltchaGrpcVerifier(
|
||||||
|
AltchaService.AltchaServiceClient client,
|
||||||
|
IOptions<AltchaGrpcOptions> options,
|
||||||
|
ILogger<AltchaGrpcVerifier> logger)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_options = options;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AltchaVerifyResult> VerifyAsync(string payload, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var opts = _options.Value;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var metadata = await AltchaCallCredentials.BuildMetadataAsync(opts, cancellationToken);
|
||||||
|
var deadline = DateTime.UtcNow.Add(opts.CallTimeout);
|
||||||
|
|
||||||
|
var response = await _client.VerifyChallengeAsync(
|
||||||
|
new VerifyChallengeRequest { Payload = payload },
|
||||||
|
headers: metadata,
|
||||||
|
deadline: deadline,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return response.Ok
|
||||||
|
? AltchaVerifyResult.Success
|
||||||
|
: AltchaVerifyResult.Fail(string.IsNullOrEmpty(response.Reason) ? "invalid" : response.Reason);
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Altcha verify timed out against {Endpoint}.", opts.Endpoint);
|
||||||
|
return AltchaVerifyResult.Fail("verify-timeout");
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Altcha service unavailable at {Endpoint}.", opts.Endpoint);
|
||||||
|
return AltchaVerifyResult.Fail("service-unavailable");
|
||||||
|
}
|
||||||
|
catch (RpcException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Altcha verify failed against {Endpoint}: {Status}", opts.Endpoint, ex.StatusCode);
|
||||||
|
return AltchaVerifyResult.Fail("rpc-error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
Svrnty.CQRS.Altcha.Grpc/Protos/altcha.proto
Normal file
69
Svrnty.CQRS.Altcha.Grpc/Protos/altcha.proto
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package svrnty.cqrs.altcha.v1;
|
||||||
|
|
||||||
|
option go_package = "svrnty.cqrs.altcha.v1;altchapb";
|
||||||
|
option csharp_namespace = "Svrnty.CQRS.Altcha.Grpc";
|
||||||
|
|
||||||
|
// AltchaService is the wire contract between any backend that gates
|
||||||
|
// commands/queries with the [Altcha] attribute and a self-hosted Altcha
|
||||||
|
// implementation. The default .NET client lives in this package; a
|
||||||
|
// reference Go server lives in plan-b's projects/altcha (which vendors
|
||||||
|
// this proto). Keep this file the source of truth for both sides.
|
||||||
|
service AltchaService {
|
||||||
|
// Mint a fresh challenge for the widget to solve. Server-stateless:
|
||||||
|
// the response embeds an HMAC signature that VerifyChallenge re-checks.
|
||||||
|
rpc CreateChallenge(CreateChallengeRequest) returns (Challenge);
|
||||||
|
|
||||||
|
// Verify a widget-produced solution payload. The server checks
|
||||||
|
// signature + expiry + PoW correctness, then atomically claims the
|
||||||
|
// challenge in a replay-protection cache (e.g. Redis SETNX) so each
|
||||||
|
// solution is single-use across all server replicas.
|
||||||
|
rpc VerifyChallenge(VerifyChallengeRequest) returns (VerifyChallengeResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateChallengeRequest {
|
||||||
|
// Optional per-request complexity override (PoW search-space upper
|
||||||
|
// bound). Higher = slower client solve. Server clamps to its
|
||||||
|
// configured min/max. Omit to use the server default.
|
||||||
|
optional uint32 complexity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Challenge {
|
||||||
|
// Hashing algorithm — e.g. "SHA-256" (v2 default). Future versions may
|
||||||
|
// upgrade to PBKDF2 / Argon2; the field lets the widget pick the
|
||||||
|
// right solver.
|
||||||
|
string algorithm = 1;
|
||||||
|
|
||||||
|
// Hex-encoded hash the client must find a preimage for. Field is
|
||||||
|
// named "challenge_hash" rather than "challenge" to avoid a C#
|
||||||
|
// property/class name collision (the generated message class is
|
||||||
|
// also named Challenge); JSON projection for the widget remaps it.
|
||||||
|
string challenge_hash = 2;
|
||||||
|
|
||||||
|
// Hex-encoded random salt. Typically embeds the expiry timestamp so
|
||||||
|
// verifiers can re-derive the TTL without server state.
|
||||||
|
string salt = 3;
|
||||||
|
|
||||||
|
// HMAC-SHA256(secret, algorithm|challenge|salt|maxnumber). Lets
|
||||||
|
// VerifyChallenge confirm the challenge was issued by this server.
|
||||||
|
string signature = 4;
|
||||||
|
|
||||||
|
// PoW search-space upper bound. Equals the complexity used.
|
||||||
|
uint32 maxnumber = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyChallengeRequest {
|
||||||
|
// Base64-encoded JSON payload produced by the Altcha widget — the
|
||||||
|
// value carried over the wire on IHasAltchaSolution.AltchaSolution.
|
||||||
|
string payload = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyChallengeResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
|
||||||
|
// Diagnostic only; not surfaced to end users. Suggested values:
|
||||||
|
// "signature-invalid", "expired", "pow-incorrect", "replayed",
|
||||||
|
// "redis-unreachable", "malformed".
|
||||||
|
string reason = 2;
|
||||||
|
}
|
||||||
64
Svrnty.CQRS.Altcha.Grpc/ServiceCollectionExtensions.cs
Normal file
64
Svrnty.CQRS.Altcha.Grpc/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Svrnty.CQRS.Altcha.Abstractions;
|
||||||
|
|
||||||
|
namespace Svrnty.CQRS.Altcha.Grpc;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the gRPC-backed <see cref="IAltchaVerifier"/> and
|
||||||
|
/// <see cref="IAltchaChallengeProvider"/>. Configure the endpoint
|
||||||
|
/// and optional service-auth token provider via the
|
||||||
|
/// <paramref name="configure"/> delegate.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// services.AddSvrntyAltcha();
|
||||||
|
/// services.AddSvrntyAltchaGrpcVerifier(opts =>
|
||||||
|
/// {
|
||||||
|
/// opts.Endpoint = "http://altcha:9090";
|
||||||
|
/// opts.TokenProvider = async ct => await tokenIssuer.GetTokenAsync("altcha", ct);
|
||||||
|
/// });
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public static IServiceCollection AddSvrntyAltchaGrpcVerifier(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<AltchaGrpcOptions> configure)
|
||||||
|
{
|
||||||
|
services.Configure(configure);
|
||||||
|
RegisterCore(services);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds <see cref="AltchaGrpcOptions"/> from a configuration section
|
||||||
|
/// (typically <c>"Altcha:Grpc"</c>) and registers the gRPC verifier
|
||||||
|
/// and challenge provider.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSvrntyAltchaGrpcVerifier(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.Configure<AltchaGrpcOptions>(configuration);
|
||||||
|
RegisterCore(services);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterCore(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddGrpcClient<AltchaService.AltchaServiceClient>((sp, client) =>
|
||||||
|
{
|
||||||
|
var opts = sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<AltchaGrpcOptions>>().Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(opts.Endpoint))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Altcha gRPC endpoint not configured. Set AltchaGrpcOptions.Endpoint " +
|
||||||
|
"(e.g. http://altcha:9090).");
|
||||||
|
client.Address = new Uri(opts.Endpoint);
|
||||||
|
});
|
||||||
|
|
||||||
|
services.TryAddSingleton<IAltchaVerifier, AltchaGrpcVerifier>();
|
||||||
|
services.TryAddSingleton<IAltchaChallengeProvider, AltchaGrpcChallengeProvider>();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Svrnty.CQRS.Altcha.Grpc/Svrnty.CQRS.Altcha.Grpc.csproj
Normal file
47
Svrnty.CQRS.Altcha.Grpc/Svrnty.CQRS.Altcha.Grpc.csproj
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<IsAotCompatible>false</IsAotCompatible>
|
||||||
|
<LangVersion>14</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|
||||||
|
<Company>Svrnty</Company>
|
||||||
|
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||||
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<RepositoryUrl>https://git.openharbor.io/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<IncludeSource>true</IncludeSource>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||||
|
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Google.Protobuf" Version="3.30.2" />
|
||||||
|
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" />
|
||||||
|
<PackageReference Include="Grpc.Tools" Version="2.71.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Svrnty.CQRS.Altcha.Abstractions\Svrnty.CQRS.Altcha.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Protobuf Include="Protos\altcha.proto" GrpcServices="Client" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha.Abstract
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha", "Svrnty.CQRS.Altcha\Svrnty.CQRS.Altcha.csproj", "{9986C034-D585-4045-9F6C-99896B8A385B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha", "Svrnty.CQRS.Altcha\Svrnty.CQRS.Altcha.csproj", "{9986C034-D585-4045-9F6C-99896B8A385B}"
|
||||||
EndProject
|
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
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -285,6 +287,18 @@ Global
|
|||||||
{9986C034-D585-4045-9F6C-99896B8A385B}.Release|x64.Build.0 = Release|Any CPU
|
{9986C034-D585-4045-9F6C-99896B8A385B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{9986C034-D585-4045-9F6C-99896B8A385B}.Release|x86.ActiveCfg = Release|Any CPU
|
{9986C034-D585-4045-9F6C-99896B8A385B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{9986C034-D585-4045-9F6C-99896B8A385B}.Release|x86.Build.0 = Release|Any CPU
|
{9986C034-D585-4045-9F6C-99896B8A385B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{628DE10C-FCDB-418B-8341-FA246BBCF70E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{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
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user